def resolve(name): resp = requests.get(CIRROS_URL, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError( 'Failed to fetch http://download.cirros-cloud.net/, ' 'status code %d' % resp.status_code) if name == 'cirros': versions = [] dir_re = re.compile(r'.*<a href="([0-9]+\.[0-9]+\.[0-9]+)/">.*/</a>.*') for line in resp.text.split('\n'): m = dir_re.match(line) if m: versions.append(m.group(1)) LOG.withField('versions', versions).info('Found cirros versions') vernum = versions[-1] else: try: # Name is assumed to be in the form cirros:0.4.0 _, vernum = name.split(':') except Exception: raise exceptions.VersionSpecificationError( 'Cannot parse version: %s' % name) url = config.parsed.get('DOWNLOAD_URL_CIRROS') % {'vernum': vernum} log = LOG.withField('url', url) # Retrieve check sum file checksum_url = CIRROS_URL + '/' + vernum + '/MD5SUMS' resp = requests.get(checksum_url, headers={'User-Agent': util.get_user_agent()}) log.withField('checksum_url', checksum_url).withField( 'resp', resp).debug("Checksum request response") if resp.status_code != 200: # Cirros does not always have a checksum file available log.warning('Unable to retrieve MD5SUMS') return url, None sum_re = re.compile(r'^([0-9a-f]+) .*' + 'cirros-' + vernum + '-x86_64-disk.img') checksum = None for line in resp.text.split('\n'): m = sum_re.match(line) if m: checksum = m.group(1) break if not checksum_url: log.warning('Did not find checksum') log.withField('checksum', checksum).info('Checksum retrieval') return (url, checksum)
def wrapper(*args, **kwargs): if config.parsed.get('NODE_IP') != config.parsed.get( 'NETWORK_NODE_IP'): admin_token = util.get_api_token( 'http://%s:%d' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT')), namespace='system') r = requests.request(flask.request.environ['REQUEST_METHOD'], 'http://%s:%d%s' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT'), flask.request.environ['PATH_INFO']), data=flask.request.data, headers={ 'Authorization': admin_token, 'User-Agent': util.get_user_agent() }) logutil.info( None, 'Returning proxied request: %d, %s' % (r.status_code, r.text)) resp = flask.Response(r.text, mimetype='application/json') resp.status_code = r.status_code return resp return func(*args, **kwargs)
def wrapper(*args, **kwargs): i = kwargs.get('instance_from_db_virt') if i and i.db_entry['node'] != config.parsed.get('NODE_NAME'): url = 'http://%s:%d%s' % (i.db_entry['node'], config.parsed.get('API_PORT'), flask.request.environ['PATH_INFO']) api_token = util.get_api_token( 'http://%s:%d' % (i.db_entry['node'], config.parsed.get('API_PORT')), namespace=get_jwt_identity()) r = requests.request(flask.request.environ['REQUEST_METHOD'], url, data=json.dumps(flask_get_post_body()), headers={ 'Authorization': api_token, 'User-Agent': util.get_user_agent() }) logutil.info( None, 'Proxied %s %s returns: %d, %s' % (flask.request.environ['REQUEST_METHOD'], url, r.status_code, r.text)) resp = flask.Response(r.text, mimetype='application/json') resp.status_code = r.status_code return resp return func(*args, **kwargs)
def resolve(name): resp = requests.get(CIRROS_URL, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError( 'Failed to fetch http://download.cirros-cloud.net/, ' 'status code %d' % resp.status_code) if name == 'cirros': versions = [] dir_re = re.compile(r'.*<a href="([0-9]+\.[0-9]+\.[0-9]+)/">.*/</a>.*') for line in resp.text.split('\n'): m = dir_re.match(line) if m: versions.append(m.group(1)) LOG.info('Found cirros versions: %s' % versions) vernum = versions[-1] else: try: # Name is assumed to be in the form cirros:0.4.0 _, vernum = name.split(':') except Exception: raise exceptions.VersionSpecificationError( 'Cannot parse version: %s' % name) return (config.parsed.get('DOWNLOAD_URL_CIRROS') % {'vernum': vernum})
def post(self, netblock=None, provide_dhcp=None, provide_nat=None, name=None, namespace=None): try: ipaddress.ip_network(netblock) except ValueError as e: return error(400, 'cannot parse netblock: %s' % e) if not namespace: namespace = get_jwt_identity() # If accessing a foreign name namespace, we need to be an admin if get_jwt_identity() not in [namespace, 'system']: return error( 401, 'only admins can create resources in a different namespace') network = db.allocate_network(netblock, provide_dhcp, provide_nat, name, namespace) db.add_event('network', network['uuid'], 'api', 'create', None, None) # Networks should immediately appear on the network node with db.get_lock('sf/network/%s' % network['uuid'], ttl=900) as _: if config.parsed.get('NODE_IP') == config.parsed.get( 'NETWORK_NODE_IP'): n = net.from_db(network['uuid']) if not n: LOG.info( 'network(%s): network not found, genuinely missing' % network['uuid']) return error(404, 'network not found') n.create() n.ensure_mesh() else: admin_token = util.get_api_token( 'http://%s:%d' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT')), namespace=namespace) requests.request('put', ('http://%s:%d/deploy_network_node' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT'))), data=json.dumps({'uuid': network['uuid']}), headers={ 'Authorization': admin_token, 'User-Agent': util.get_user_agent() }) db.add_event('network', network['uuid'], 'api', 'created', None, None) db.update_network_state(network['uuid'], 'created') # Initialise metadata db.persist_metadata('network', network['uuid'], {}) return network
def _open_connection(self): resp = requests.get(self.url, allow_redirects=True, stream=True, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError( 'Failed to fetch HEAD of %s (status code %d)' % (self.url, resp.status_code)) return resp
def _requires_fetch(self): resp = requests.get(self.url, allow_redirects=True, stream=True, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError( 'Failed to fetch HEAD of %s (status code %d)' % (self.url, resp.status_code)) dirty_fields = {} for field in VALIDATED_IMAGE_FIELDS: if self.info.get(field) != resp.headers.get(field): dirty_fields[field] = { 'before': self.info.get(field), 'after': resp.headers.get(field) } return dirty_fields, resp
def resolve(name): resp = requests.get(UBUNTU_URL, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError('Failed to fetch %s, status code %d' % (UBUNTU_URL, resp.status_code)) num_to_name = {} name_to_num = {} dir_re = re.compile( r'.*<a href="(.*)/">.*Ubuntu Server ([0-9]+\.[0-9]+).*') for line in resp.text.split('\n'): m = dir_re.match(line) if m: num_to_name[m.group(2)] = m.group(1) name_to_num[m.group(1)] = m.group(2) logutil.info(None, 'Found ubuntu versions: %s' % num_to_name) vernum = None vername = None if name == 'ubuntu': vernum = sorted(num_to_name.keys())[-1] vername = num_to_name[vernum] else: try: # Name is assumed to be in the form ubuntu:18.04 or ubuntu:bionic _, version = name.split(':') if version in num_to_name: vernum = version vername = num_to_name[version] else: vername = version vernum = name_to_num[version] except Exception: raise exceptions.VersionSpecificationError( 'Cannot parse version: %s' % name) return (config.parsed.get('DOWNLOAD_URL_UBUNTU') % { 'vernum': vernum, 'vername': vername })
def remove_dhcp(self): if config.parsed.get('NODE_IP') == config.parsed.get('NETWORK_NODE_IP'): subst = self.subst_dict() with util.RecordedOperation('remove dhcp', self) as _: with db.get_lock('sf/net/%s' % self.uuid, ttl=120) as _: d = dhcp.DHCP(self.uuid, subst['vx_veth_inner']) d.remove_dhcpd() else: admin_token = util.get_api_token( 'http://%s:%d' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT')), namespace='system') requests.request( 'put', ('http://%s:%d/remove_dhcp' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT'))), data=json.dumps({'uuid': self.uuid}), headers={'Authorization': admin_token, 'User-Agent': util.get_user_agent()})
def requires_fetch(image_url): hashed_image_url = _hash_image_url(image_url) hashed_image_path = os.path.join(_get_cache_path(), hashed_image_url) info = _read_info(image_url, hashed_image_url, hashed_image_path) resp = requests.get(image_url, allow_redirects=True, stream=True, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError( 'Failed to fetch HEAD of %s (status code %d)' % (image_url, resp.status_code)) image_dirty = False for field in VALIDATED_IMAGE_FIELDS: if info.get(field) != resp.headers.get(field): image_dirty = True return hashed_image_path, info, image_dirty, resp
def create(self): subst = self.subst_dict() with db.get_lock('sf/net/%s' % self.uuid, ttl=120) as _: if not util.check_for_interface(subst['vx_interface']): with util.RecordedOperation('create vxlan interface', self) as _: processutils.execute( 'ip link add %(vx_interface)s type vxlan id %(vx_id)s ' 'dev %(physical_interface)s dstport 0' % subst, shell=True) processutils.execute( 'sysctl -w net.ipv4.conf.%(vx_interface)s.arp_notify=1' % subst, shell=True) if not util.check_for_interface(subst['vx_bridge']): with util.RecordedOperation('create vxlan bridge', self) as _: processutils.execute( 'ip link add %(vx_bridge)s type bridge' % subst, shell=True) processutils.execute( 'ip link set %(vx_interface)s master %(vx_bridge)s' % subst, shell=True) processutils.execute( 'ip link set %(vx_interface)s up' % subst, shell=True) processutils.execute( 'ip link set %(vx_bridge)s up' % subst, shell=True) processutils.execute( 'sysctl -w net.ipv4.conf.%(vx_bridge)s.arp_notify=1' % subst, shell=True) processutils.execute( 'brctl setfd %(vx_bridge)s 0' % subst, shell=True) processutils.execute( 'brctl stp %(vx_bridge)s off' % subst, shell=True) processutils.execute( 'brctl setageing %(vx_bridge)s 0' % subst, shell=True) if config.parsed.get('NODE_IP') == config.parsed.get('NETWORK_NODE_IP'): if not os.path.exists('/var/run/netns/%(netns)s' % subst): with util.RecordedOperation('create netns', self) as _: processutils.execute( 'ip netns add %(netns)s' % subst, shell=True) if not util.check_for_interface(subst['vx_veth_outer']): with util.RecordedOperation('create router veth', self) as _: processutils.execute( 'ip link add %(vx_veth_outer)s type veth peer name %(vx_veth_inner)s' % subst, shell=True) processutils.execute( 'ip link set %(vx_veth_inner)s netns %(netns)s' % subst, shell=True) processutils.execute( 'brctl addif %(vx_bridge)s %(vx_veth_outer)s' % subst, shell=True) processutils.execute( 'ip link set %(vx_veth_outer)s up' % subst, shell=True) processutils.execute( '%(in_netns)s ip link set %(vx_veth_inner)s up' % subst, shell=True) processutils.execute( '%(in_netns)s ip addr add %(router)s/%(netmask)s dev %(vx_veth_inner)s' % subst, shell=True) if not util.check_for_interface(subst['physical_veth_outer']): with util.RecordedOperation('create physical veth', self) as _: processutils.execute( 'ip link add %(physical_veth_outer)s type veth peer name ' '%(physical_veth_inner)s' % subst, shell=True) processutils.execute( 'brctl addif %(physical_bridge)s %(physical_veth_outer)s' % subst, shell=True) processutils.execute( 'ip link set %(physical_veth_outer)s up' % subst, shell=True) processutils.execute( 'ip link set %(physical_veth_inner)s netns %(netns)s' % subst, shell=True) self.deploy_nat() self.update_dhcp() else: admin_token = util.get_api_token( 'http://%s:%d' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT')), namespace='system') requests.request( 'put', ('http://%s:%d/deploy_network_node' % (config.parsed.get('NETWORK_NODE_IP'), config.parsed.get('API_PORT'))), data=json.dumps({'uuid': self.uuid}), headers={'Authorization': admin_token, 'User-Agent': util.get_user_agent()})
def resolve(name): resp = requests.get(UBUNTU_URL, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError('Failed to fetch %s, status code %d' % (UBUNTU_URL, resp.status_code)) num_to_name = {} name_to_num = {} dir_re = re.compile( r'.*<a href="(.*)/">.*Ubuntu Server ([0-9]+\.[0-9]+).*') for line in resp.text.split('\n'): m = dir_re.match(line) if m: num_to_name[m.group(2)] = m.group(1) name_to_num[m.group(1)] = m.group(2) LOG.withField('versions', num_to_name).info('Found ubuntu versions') vernum = None vername = None if name == 'ubuntu': vernum = sorted(num_to_name.keys())[-1] vername = num_to_name[vernum] else: try: # Name is assumed to be in the form ubuntu:18.04 or ubuntu:bionic _, version = name.split(':') if version in num_to_name: vernum = version vername = num_to_name[version] else: vername = version vernum = name_to_num[version] except Exception: raise exceptions.VersionSpecificationError( 'Cannot parse version: %s' % name) url = (config.parsed.get('DOWNLOAD_URL_UBUNTU') % { 'vernum': vernum, 'vername': vername }) log = LOG.withField('url', url) # Retrieve check sum file checksum_url = UBUNTU_URL + '/' + vername + '/current/MD5SUMS' resp = requests.get(checksum_url, headers={'User-Agent': util.get_user_agent()}) if resp.status_code != 200: raise exceptions.HTTPError('Failed to fetch %s, status code %d' % (checksum_url, resp.status_code)) sum_re = re.compile(r'^([0-9a-f]+) .*' + vername + '-server-cloudimg-amd64.img') checksum = None for line in resp.text.split('\n'): m = sum_re.match(line) if m: checksum = m.group(1) break if not checksum_url: log.warning('Did not find checksum') log.withField('checksum', checksum).info('Checksum check') return (url, checksum)
def post(self, name=None, cpus=None, memory=None, network=None, disk=None, ssh_key=None, user_data=None, placed_on=None, namespace=None, instance_uuid=None): global SCHEDULER # We need to sanitise the name so its safe for DNS name = re.sub(r'([^a-zA-Z0-9_\-])', '', name) if not namespace: namespace = get_jwt_identity() # If accessing a foreign namespace, we need to be an admin if get_jwt_identity() not in [namespace, 'system']: return error( 401, 'only admins can create resources in a different namespace') # The instance needs to exist in the DB before network interfaces are created if not instance_uuid: instance_uuid = str(uuid.uuid4()) db.add_event('instance', instance_uuid, 'uuid allocated', None, None, None) # Create instance object instance = virt.from_db(instance_uuid) if instance: if get_jwt_identity() not in [ instance.db_entry['namespace'], 'system' ]: LOG.info('instance(%s): instance not found, ownership test' % instance_uuid) return error(404, 'instance not found') if not instance: instance = virt.from_definition(uuid=instance_uuid, name=name, disks=disk, memory_mb=memory, vcpus=cpus, ssh_key=ssh_key, user_data=user_data, owner=namespace) if not SCHEDULER: SCHEDULER = scheduler.Scheduler() # Have we been placed? if not placed_on: candidates = SCHEDULER.place_instance(instance, network) if len(candidates) == 0: db.add_event('instance', instance_uuid, 'schedule', 'failed', None, 'insufficient resources') db.update_instance_state(instance_uuid, 'error') return error(507, 'insufficient capacity') placed_on = candidates[0] db.place_instance(instance_uuid, placed_on) db.add_event('instance', instance_uuid, 'placement', None, None, placed_on) else: try: candidates = SCHEDULER.place_instance(instance, network, candidates=[placed_on]) if len(candidates) == 0: db.add_event('instance', instance_uuid, 'schedule', 'failed', None, 'insufficient resources') db.update_instance_state(instance_uuid, 'error') return error(507, 'insufficient capacity') except scheduler.CandidateNodeNotFoundException as e: return error(404, 'node not found: %s' % e) # Have we been placed on a different node? if not placed_on == config.parsed.get('NODE_NAME'): body = flask_get_post_body() body['placed_on'] = placed_on body['instance_uuid'] = instance_uuid body['namespace'] = namespace token = util.get_api_token( 'http://%s:%d' % (placed_on, config.parsed.get('API_PORT')), namespace=namespace) r = requests.request('POST', 'http://%s:%d/instances' % (placed_on, config.parsed.get('API_PORT')), data=json.dumps(body), headers={ 'Authorization': token, 'User-Agent': util.get_user_agent() }) LOG.info('Returning proxied request: %d, %s' % (r.status_code, r.text)) resp = flask.Response(r.text, mimetype='application/json') resp.status_code = r.status_code return resp # Check we can get the required IPs nets = {} allocations = {} def error_with_cleanup(status_code, message): for network_uuid in allocations: n = net.from_db(network_uuid) for addr, _ in allocations[network_uuid]: with db.get_lock('sf/ipmanager/%s' % n.uuid, ttl=120) as _: ipm = db.get_ipmanager(n.uuid) ipm.release(addr) db.persist_ipmanager(n.uuid, ipm.save()) return error(status_code, message) order = 0 if network: for netdesc in network: if 'network_uuid' not in netdesc or not netdesc['network_uuid']: return error_with_cleanup(404, 'network not specified') if netdesc['network_uuid'] not in nets: n = net.from_db(netdesc['network_uuid']) if not n: return error_with_cleanup( 404, 'network %s not found' % netdesc['network_uuid']) nets[netdesc['network_uuid']] = n n.create() with db.get_lock('sf/ipmanager/%s' % netdesc['network_uuid'], ttl=120) as _: db.add_event('network', netdesc['network_uuid'], 'allocate address', None, None, instance_uuid) allocations.setdefault(netdesc['network_uuid'], []) ipm = db.get_ipmanager(netdesc['network_uuid']) if 'address' not in netdesc or not netdesc['address']: netdesc['address'] = ipm.get_random_free_address() else: if not ipm.reserve(netdesc['address']): return error_with_cleanup( 409, 'address %s in use' % netdesc['address']) db.persist_ipmanager(netdesc['network_uuid'], ipm.save()) allocations[netdesc['network_uuid']].append( (netdesc['address'], order)) if 'model' not in netdesc or not netdesc['model']: netdesc['model'] = 'virtio' db.create_network_interface(str(uuid.uuid4()), netdesc, instance_uuid, order) order += 1 # Initialise metadata db.persist_metadata('instance', instance_uuid, {}) # Now we can start the instance with db.get_lock('sf/instance/%s' % instance.db_entry['uuid'], ttl=900) as lock: with util.RecordedOperation('ensure networks exist', instance) as _: for network_uuid in nets: n = nets[network_uuid] n.ensure_mesh() n.update_dhcp() with util.RecordedOperation('instance creation', instance) as _: instance.create(lock=lock) for iface in db.get_instance_interfaces(instance.db_entry['uuid']): db.update_network_interface_state(iface['uuid'], 'created') return db.get_instance(instance_uuid)