def dockerhost(self): """This is a helper method to get the machine representing the host""" if self._dockerhost is not None: return self._dockerhost try: # Find dockerhost from database. machine = Machine.objects.get(cloud=self.cloud, machine_type='container-host') except Machine.DoesNotExist: try: # Find dockerhost with previous format from database. machine = Machine.objects.get( cloud=self.cloud, # Nested query. Trailing underscores to avoid conflict # with mongo's $type operator. See: # https://github.com/MongoEngine/mongoengine/issues/1410 **{'extra__tags__type__': 'docker_host'}) except Machine.DoesNotExist: # Create dockerrhost machine. machine = Machine(cloud=self.cloud, machine_type='container-host') # Update dockerhost machine model fields. changed = False for attr, val in { 'name': self.cloud.title, 'hostname': self.cloud.host, 'machine_type': 'container-host' }.iteritems(): if getattr(machine, attr) != val: setattr(machine, attr, val) changed = True if not machine.machine_id: machine.machine_id = machine.id changed = True try: ip_addr = socket.gethostbyname(machine.hostname) except socket.gaierror: pass else: is_private = netaddr.IPAddress(ip_addr).is_private() ips = machine.private_ips if is_private else machine.public_ips if ip_addr not in ips: ips.insert(0, ip_addr) changed = True if changed: machine.save() self._dockerhost = machine return machine
def list_machines(self): """Return list of machines for cloud A list of nodes is fetched from libcloud, the data is processed, stored on machine models, and a list of machine models is returned. Subclasses SHOULD NOT override or extend this method. There are instead a number of methods that are called from this method, to allow subclasses to modify the data according to the specific of their cloud type. These methods currently are: `self._list_machines__fetch_machines` `self._list_machines__machine_actions` `self._list_machines__postparse_machine` `self._list_machines__cost_machine` `self._list_machines__fetch_generic_machines` Subclasses that require special handling should override these, by default, dummy methods. """ # Try to query list of machines from provider API. try: nodes = self._list_machines__fetch_machines() log.info("List nodes returned %d results for %s.", len(nodes), self.cloud) except InvalidCredsError as exc: log.warning("Invalid creds on running list_nodes on %s: %s", self.cloud, exc) raise CloudUnauthorizedError(msg=exc.message) except ssl.SSLError as exc: log.error("SSLError on running list_nodes on %s: %s", self.cloud, exc) raise SSLError(exc=exc) except Exception as exc: log.exception("Error while running list_nodes on %s", self.cloud) raise CloudUnavailableError(exc=exc) machines = [] now = datetime.datetime.utcnow() # Process each machine in returned list. # Store previously unseen machines separately. new_machines = [] for node in nodes: # Fetch machine mongoengine model from db, or initialize one. try: machine = Machine.objects.get(cloud=self.cloud, machine_id=node.id) except Machine.DoesNotExist: machine = Machine(cloud=self.cloud, machine_id=node.id).save() new_machines.append(machine) # Update machine_model's last_seen fields. machine.last_seen = now machine.missing_since = None # Get misc libcloud metadata. image_id = str(node.image or node.extra.get('imageId') or node.extra.get('image_id') or node.extra.get('image') or '') size = (node.size or node.extra.get('flavorId') or node.extra.get('instancetype')) machine.name = node.name machine.image_id = image_id machine.size = size machine.state = config.STATES[node.state] machine.private_ips = node.private_ips machine.public_ips = node.public_ips # Set machine extra dict. # Make sure we don't meet any surprises when we try to json encode # later on in the HTTP response. extra = self._list_machines__get_machine_extra(machine, node) for key, val in extra.items(): try: json.dumps(val) except TypeError: extra[key] = str(val) machine.extra = extra # Set machine hostname if machine.extra.get('dns_name'): machine.hostname = machine.extra['dns_name'] else: ips = machine.public_ips + machine.private_ips if not ips: ips = [] for ip in ips: if ip and ':' not in ip: machine.hostname = ip break # Get machine tags from db tags = {tag.key: tag.value for tag in Tag.objects( owner=self.cloud.owner, resource=machine, ).only('key', 'value')} # Get machine creation date. try: created = self._list_machines__machine_creation_date(machine, node) if created: machine.created = get_datetime(created) except Exception as exc: log.exception("Error finding creation date for %s in %s.", self.cloud, machine) # TODO: Consider if we should fall back to using current date. # if not machine_model.created: # machine_model.created = datetime.datetime.utcnow() # Update with available machine actions. try: self._list_machines__machine_actions(machine, node) except Exception as exc: log.exception("Error while finding machine actions " "for machine %s:%s for %s", machine.id, node.name, self.cloud) # Apply any cloud/provider specific post processing. try: self._list_machines__postparse_machine(machine, node) except Exception as exc: log.exception("Error while post parsing machine %s:%s for %s", machine.id, node.name, self.cloud) # Apply any cloud/provider cost reporting. try: def parse_num(num): try: return float(num or 0) except (ValueError, TypeError): log.warning("Can't parse %r as float.", num) return 0 month_days = calendar.monthrange(now.year, now.month)[1] cph = parse_num(tags.get('cost_per_hour')) cpm = parse_num(tags.get('cost_per_month')) if not (cph or cpm) or cph > 100 or cpm > 100 * 24 * 31: cph, cpm = map(parse_num, self._list_machines__cost_machine(machine, node)) if not cph: cph = float(cpm) / month_days / 24 elif not cpm: cpm = cph * 24 * month_days machine.cost.hourly = cph machine.cost.monthly = cpm except Exception as exc: log.exception("Error while calculating cost " "for machine %s:%s for %s", machine.id, node.name, self.cloud) if node.state.lower() == 'terminated': machine.cost.hourly = 0 machine.cost.monthly = 0 # Save all changes to machine model on the database. try: machine.save() except me.ValidationError as exc: log.error("Error adding %s: %s", machine.name, exc.to_dict()) raise BadRequestError({"msg": exc.message, "errors": exc.to_dict()}) except me.NotUniqueError as exc: log.error("Machine %s not unique error: %s", machine.name, exc) raise ConflictError("Machine with this name already exists") machines.append(machine) # Append generic-type machines, which aren't handled by libcloud. for machine in self._list_machines__fetch_generic_machines(): machine.last_seen = now machine.missing_since = None machine.state = config.STATES[NodeState.UNKNOWN] for action in ('start', 'stop', 'reboot', 'destroy', 'rename', 'resume', 'suspend', 'undefine'): setattr(machine.actions, action, False) machine.actions.tag = True # allow reboot action for bare metal with key associated if machine.key_associations: machine.actions.reboot = True machine.save() machines.append(machine) # Set last_seen on machine models we didn't see for the first time now. Machine.objects(cloud=self.cloud, id__nin=[m.id for m in machines], missing_since=None).update(missing_since=now) # Update RBAC Mappings given the list of nodes seen for the first time. self.cloud.owner.mapper.update(new_machines) # Update machine counts on cloud and org. # FIXME: resolve circular import issues from mist.api.clouds.models import Cloud self.cloud.machine_count = len(machines) self.cloud.save() self.cloud.owner.total_machine_count = sum( cloud.machine_count for cloud in Cloud.objects( owner=self.cloud.owner, deleted=None ).only('machine_count') ) self.cloud.owner.save() # Close libcloud connection try: self.disconnect() except Exception as exc: log.warning("Error while closing connection: %r", exc) return machines
def add_machine(self, name, host='', ssh_user='******', ssh_port=22, ssh_key=None, os_type='unix', rdp_port=3389, fail_on_error=True): """Add machine to this dummy Cloud This is a special method that exists only on this Cloud subclass. """ old_machines = [m.as_dict() for m in self.cloud.ctl.compute.list_cached_machines()] # FIXME: Move ssh command to Machine controller once it is migrated. from mist.api.methods import ssh_command try: ssh_port = int(ssh_port) except (ValueError, TypeError): ssh_port = 22 try: rdp_port = int(rdp_port) except (ValueError, TypeError): rdp_port = 3389 if ssh_key: ssh_key = Key.objects.get(owner=self.cloud.owner, id=ssh_key, deleted=None) from mist.api.machines.models import Machine # Create and save machine entry to database. machine = Machine( cloud=self.cloud, name=name, machine_id=uuid.uuid4().hex, os_type=os_type, ssh_port=ssh_port, rdp_port=rdp_port, last_seen=datetime.datetime.utcnow() ) if host: # Sanitize inputs. host = sanitize_host(host) check_host(host) machine.hostname = host if is_private_subnet(socket.gethostbyname(host)): machine.private_ips = [host] else: machine.public_ips = [host] machine.save(write_concern={'w': 1, 'fsync': True}) # Attempt to connect. if os_type == 'unix' and ssh_key: if not ssh_user: ssh_user = '******' # Try to connect. If it works, it will create the association. try: if not host: raise BadRequestError("You have specified an SSH key but " "machine hostname is empty.") to_tunnel(self.cloud.owner, host) # May raise VPNTunnelError ssh_command( self.cloud.owner, self.cloud.id, machine.id, host, 'uptime', key_id=ssh_key.id, username=ssh_user, port=ssh_port ) except MachineUnauthorizedError as exc: if fail_on_error: machine.delete() raise CloudUnauthorizedError(exc) except ServiceUnavailableError as exc: if fail_on_error: machine.delete() raise MistError("Couldn't connect to host '%s'." % host) except: if fail_on_error: machine.delete() raise if amqp_owner_listening(self.cloud.owner.id): new_machines = self.cloud.ctl.compute.list_cached_machines() self.cloud.ctl.compute.produce_and_publish_patch( old_machines, new_machines) return machine
def autoconfigure(self, owner, cloud_id, machine_id, key_id=None, username=None, password=None, port=22): """Autoconfigure SSH client. This will do its best effort to find a suitable key and username and will try to connect. If it fails it raises MachineUnauthorizedError, otherwise it initializes self and returns a (key_id, ssh_user) tuple. If connection succeeds, it updates the association information in the key with the current timestamp and the username used to connect. """ log.info("autoconfiguring Shell for machine %s:%s", cloud_id, machine_id) cloud = Cloud.objects.get(owner=owner, id=cloud_id, deleted=None) try: machine = Machine.objects.get(cloud=cloud, machine_id=machine_id) except me.DoesNotExist: machine = Machine(cloud=cloud, machine_id=machine_id) if key_id: keys = [Key.objects.get(owner=owner, id=key_id, deleted=None)] else: keys = [ key_assoc.keypair for key_assoc in machine.key_associations if isinstance(key_assoc.keypair, Key) ] if username: users = [username] else: users = list( set([ key_assoc.ssh_user for key_assoc in machine.key_associations if key_assoc.ssh_user ])) if not users: for name in [ 'root', 'ubuntu', 'ec2-user', 'user', 'azureuser', 'core', 'centos', 'cloud-user', 'fedora' ]: if not name in users: users.append(name) if port != 22: ports = [port] else: ports = list( set([key_assoc.port for key_assoc in machine.key_associations])) if 22 not in ports: ports.append(22) # store the original destination IP to prevent rewriting it when NATing ssh_host = self.host for key in keys: for ssh_user in users: for port in ports: try: # store the original ssh port in case of NAT # by the OpenVPN server ssh_port = port self.host, port = dnat(owner, ssh_host, port) log.info("ssh -i %s %s@%s:%s", key.name, ssh_user, self.host, port) cert_file = '' if isinstance(key, SignedSSHKey): cert_file = key.certificate self.connect(username=ssh_user, key=key, password=password, cert_file=cert_file, port=port) except MachineUnauthorizedError: continue retval, resp = self.command('uptime') new_ssh_user = None if 'Please login as the user ' in resp: new_ssh_user = resp.split()[5].strip('"') elif 'Please login as the' in resp: # for EC2 Amazon Linux machines, usually with ec2-user new_ssh_user = resp.split()[4].strip('"') if new_ssh_user: log.info("retrying as %s", new_ssh_user) try: self.disconnect() cert_file = '' if isinstance(key, SignedSSHKey): cert_file = key.certificate self.connect(username=new_ssh_user, key=key, password=password, cert_file=cert_file, port=port) ssh_user = new_ssh_user except MachineUnauthorizedError: continue # we managed to connect successfully, return # but first update key updated = False for key_assoc in machine.key_associations: if key_assoc.keypair == key: key_assoc.ssh_user = ssh_user updated = True trigger_session_update_flag = True break if not updated: trigger_session_update_flag = True # in case of a private host do NOT update the key # associations with the port allocated by the OpenVPN # server, instead use the original ssh_port key_assoc = KeyAssociation(keypair=key, ssh_user=ssh_user, port=ssh_port, sudo=self.check_sudo()) machine.key_associations.append(key_assoc) machine.save() if trigger_session_update_flag: trigger_session_update(owner.id, ['keys']) return key.name, ssh_user raise MachineUnauthorizedError("%s:%s" % (cloud_id, machine_id))
def add_machine(self, host, ssh_user='******', ssh_port=22, ssh_key=None, **kwargs): try: ssh_port = int(ssh_port) except (ValueError, TypeError): ssh_port = 22 if not ssh_key: raise RequiredParameterMissingError('machine_key') try: ssh_key = Key.objects.get(owner=self.cloud.owner, id=ssh_key, deleted=None) except Key.DoesNotExist: raise NotFoundError("Key does not exist.") images_location = kwargs.get('images_location', '/var/lib/libvirt/images') extra = { 'images_location': images_location, 'tags': {'type': 'hypervisor'}, 'username': ssh_user } from mist.api.machines.models import Machine # Create and save machine entry to database. # first check if the host has already been added to the cloud try: machine = Machine.objects.get(cloud=self.cloud, machine_id=host.replace('.', '-')) machine.name = kwargs.get('name') or host machine.ssh_port = ssh_port machine.extra = extra machine.last_seen = datetime.datetime.utcnow() machine.missing_since = None except me.DoesNotExist: machine = Machine( cloud=self.cloud, name=kwargs.get('name') or host, hostname=host, machine_id=host.replace('.', '-'), ssh_port=ssh_port, extra=extra, state=NodeState.RUNNING, last_seen=datetime.datetime.utcnow(), ) # Sanitize inputs. host = sanitize_host(host) check_host(host) machine.hostname = host if is_private_subnet(socket.gethostbyname(host)): machine.private_ips = [host] else: machine.public_ips = [host] machine.save(write_concern={'w': 1, 'fsync': True}) # associate key and attempt to connect try: machine.ctl.associate_key(ssh_key, username=ssh_user, port=ssh_port) except MachineUnauthorizedError as exc: log.error("Could not connect to host %s." % host) machine.delete() raise CloudUnauthorizedError(exc) except ServiceUnavailableError as exc: log.error("Could not connect to host %s." % host) machine.delete() raise MistError("Couldn't connect to host '%s'." % host) if amqp_owner_listening(self.cloud.owner.id): old_machines = [] for cached_machine in \ self.cloud.ctl.compute.list_cached_machines(): # make sure that host just added becomes visible if cached_machine.id != machine.id: old_machines.append(cached_machine) old_machines = [m.as_dict() for m in old_machines] new_machines = self.cloud.ctl.compute.list_machines() self.cloud.ctl.compute.produce_and_publish_patch( old_machines, new_machines) return machine
def add(self, fail_on_error=True, fail_on_invalid_params=False, **kwargs): from mist.api.machines.models import Machine if not kwargs.get('hosts'): raise RequiredParameterMissingError('hosts') try: self.cloud.save() except me.ValidationError as exc: raise BadRequestError({'msg': str(exc), 'errors': exc.to_dict()}) except me.NotUniqueError: raise CloudExistsError("Cloud with name %s already exists" % self.cloud.title) total_errors = {} for _host in kwargs['hosts']: self._add__preparse_kwargs(_host) errors = {} for key in list(_host.keys()): if key not in ('host', 'alias', 'username', 'port', 'key', 'images_location'): error = "Invalid parameter %s=%r." % (key, _host[key]) if fail_on_invalid_params: self.cloud.delete() raise BadRequestError(error) else: log.warning(error) _host.pop(key) for key in ('host', 'key'): if key not in _host or not _host.get(key): error = "Required parameter missing: %s" % key errors[key] = error if fail_on_error: self.cloud.delete() raise RequiredParameterMissingError(key) else: log.warning(error) total_errors.update({key: error}) if not errors: try: ssh_port = int(_host.get('ssh_port', 22)) except (ValueError, TypeError): ssh_port = 22 images_location = _host.get('images_location', '/var/lib/libvirt/images') extra = { 'images_location': images_location, 'tags': {'type': 'hypervisor'}, 'username': _host.get('username') } # Create and save machine entry to database. machine = Machine( cloud=self.cloud, machine_id=_host.get('host').replace('.', '-'), name=_host.get('alias') or _host.get('host'), ssh_port=ssh_port, last_seen=datetime.datetime.utcnow(), hostname=_host.get('host'), state=NodeState.RUNNING, extra=extra ) # Sanitize inputs. host = sanitize_host(_host.get('host')) check_host(_host.get('host')) machine.hostname = host if is_private_subnet(socket.gethostbyname(_host.get('host'))): machine.private_ips = [_host.get('host')] else: machine.public_ips = [_host.get('host')] try: machine.save(write_concern={'w': 1, 'fsync': True}) except me.NotUniqueError: error = 'Duplicate machine entry. Maybe the same \ host has been added twice?' if fail_on_error: self.cloud.delete() raise MistError(error) else: total_errors.update({_host.get('host'): error}) continue # associate key and attempt to connect try: machine.ctl.associate_key(_host.get('key'), username=_host.get('username'), port=ssh_port) except MachineUnauthorizedError as exc: log.error("Could not connect to host %s." % _host.get('host')) machine.delete() if fail_on_error: self.cloud.delete() raise CloudUnauthorizedError(exc) except ServiceUnavailableError as exc: log.error("Could not connect to host %s." % _host.get('host')) machine.delete() if fail_on_error: self.cloud.delete() raise MistError("Couldn't connect to host '%s'." % _host.get('host')) # check if host was added successfully # if not, delete the cloud and raise if Machine.objects(cloud=self.cloud): if amqp_owner_listening(self.cloud.owner.id): old_machines = [m.as_dict() for m in self.cloud.ctl.compute.list_cached_machines()] new_machines = self.cloud.ctl.compute.list_machines() self.cloud.ctl.compute.produce_and_publish_patch( old_machines, new_machines) self.cloud.errors = total_errors else: self.cloud.delete() raise BadRequestError(total_errors)