def reject(id, status_text=None): """Rejects a request""" sysrequest = get_request_by_id(id) #check if system is already approved if sysrequest['status'] == 2: raise Exception('Request already approved') elif sysrequest['status'] == 1: raise Exception('Request already rejected') stmt = 'UPDATE `system_request` SET `status`=1, `updated_at`=NOW(), `updated_who`=%s, `status_text`=%s WHERE `id`=%s' params = (session['username'], status_text, id) curd = g.db.cursor(mysql.cursors.DictCursor) curd.execute(stmt, params) g.db.commit() flash('Request has been rejected', 'alert-info') #email requester subject = 'System request rejected' message = ( 'Your request for a system has been rejected.\n' + '\n' + 'Request id: ' + str(sysrequest['id']) + '\n' + 'Requested at: ' + str(sysrequest['request_date']) + '\n' + 'Reason: ' + str(status_text) + '\n' + '\n' + 'For more details see https://cortex.soton.ac.uk/sysrequest/view/' + str(sysrequest['id'])) corpus = Corpus(g.db, app.config) corpus.send_email(str(sysrequest['requested_who']), subject, message)
def get_system_by_name(name, must_have_vmware_uuid=False, must_have_snow_sys_id=False): """Gets all the information about a system by its hostname.""" corpus = Corpus(g.db, app.config) return corpus.get_system_by_name(name, must_have_vmware_uuid, must_have_snow_sys_id)
def get_vm_by_system_id(id): query = 'SELECT `vmware_uuid`, `vmware_vcenter` FROM `systems_info_view` WHERE `id`=%s AND `vmware_uuid` IS NOT NULL' params = (id, ) curd = g.db.cursor(mysql.cursors.DictCursor) curd.execute(query, params) row = curd.fetchone() if row is None: raise ValueError corpus = Corpus(g.db, app.config) return corpus.vmware_get_vm_by_uuid(row['vmware_uuid'], row['vmware_vcenter'])
def api_installer_notify(): """API endpoint to allow the bonemeal installer to notify cortex that the the installation is now complete and is about to reboot.""" if 'uuid' not in request.form: app.logger.warn('Missing \'uuid\' parameter in installer notify API') abort(401) # Create a corpus (task helper) object corpus = Corpus(g.db, app.config) # VMware UUID based authentication system = cortex.lib.systems.get_system_by_vmware_uuid( request.form['uuid'].lower()) if not system: app.logger.warn( 'Could not match VMware UUID to a system for the installer notify API (UUID given: ' + request.form['uuid'].lower() + ')') abort(404) # Mark as done if 'warnings' in request.form and int(request.form['warnings']) > 0: corpus.redis_set_vm_data("notify", "done-with-warnings", uuid=system['vmware_uuid']) else: corpus.redis_set_vm_data("notify", "done", uuid=system['vmware_uuid']) return "OK"
def generate_repeatable_password(id): corpus = Corpus(g.db, app.config) return corpus.system_get_repeatable_password(id)
def get_system_by_id(id): """Gets all the information about a system by its database ID.""" corpus = Corpus(g.db, app.config) return corpus.get_system_by_id(id)
def decom_step2(id): # in this step we work out what steps to perform # then we load this into a list of steps, each step being a dictionary # this is used on the page to list the steps to the user # the list is also used to generate a JSON document which we sign using # app.config['SECRET_KEY'] and then send that onto the page as well. # load the corpus library corpus = Corpus(g.db, app.config) system = cortex.lib.systems.get_system_by_id(id) if system is None: abort(404) actions = [] systemenv = None ## Find the environment that this VM is in based off of the CMDB env if 'cmdb_environment' in system: if system['cmdb_environment'] is not None: for env in app.config['ENVIRONMENTS']: if env['name'] == system['cmdb_environment']: # We found the environment matching the system systemenv = env break ## Is the system linked to vmware? if 'vmware_uuid' in system: if system['vmware_uuid'] is not None: if len(system['vmware_uuid']) > 0: ## The system is linked to vmware - e.g. a VM exists vmobj = corpus.vmware_get_vm_by_uuid(system['vmware_uuid'], system['vmware_vcenter']) if vmobj: if vmobj.runtime.powerState == vim.VirtualMachine.PowerState.poweredOn: actions.append({ 'id': 'vm.poweroff', 'desc': 'Power off the virtual machine ' + system['name'], 'detail': 'UUID ' + system['vmware_uuid'] + ' on ' + system['vmware_vcenter'], 'data': { 'uuid': system['vmware_uuid'], 'vcenter': system['vmware_vcenter'] } }) actions.append({ 'id': 'vm.delete', 'desc': 'Delete the virtual machine ' + system['name'], 'detail': ' UUID ' + system['vmware_uuid'] + ' on ' + system['vmware_vcenter'], 'data': { 'uuid': system['vmware_uuid'], 'vcenter': system['vmware_vcenter'] } }) ## Is the system linked to service now? if 'cmdb_id' in system: if system['cmdb_id'] is not None: if len(system['cmdb_id']) > 0: if system['cmdb_is_virtual']: if system['cmdb_operational_status'] != u'Deleted': actions.append({ 'id': 'cmdb.update', 'desc': 'Mark the system as Deleted in the CMDB', 'detail': system['cmdb_id'] + " on " + app.config['SN_HOST'], 'data': system['cmdb_id'] }) else: if system['cmdb_operational_status'] != u'Decommissioned': actions.append({ 'id': 'cmdb.update', 'desc': 'Mark the system as Decommissioned in the CMDB', 'detail': system['cmdb_id'] + " on " + app.config['SN_HOST'], 'data': system['cmdb_id'] }) ## Ask infoblox if a DNS host object exists for the name of the system try: refs = corpus.infoblox_get_host_refs(system['name'] + ".soton.ac.uk") if refs is not None: for ref in refs: actions.append({ 'id': 'dns.delete', 'desc': 'Delete the DNS record ' + ref.split(':')[-1], 'detail': 'Delete the name ' + system['name'] + '.soton.ac.uk - Infoblox reference: ' + ref, 'data': ref }) except Exception as ex: flash( "Warning - An error occured when communicating with Infoblox: " + str(type(ex)) + " - " + str(ex), "alert-warning") ## Check if a puppet record exists if 'puppet_certname' in system: if system['puppet_certname'] is not None: if len(system['puppet_certname']) > 0: actions.append({ 'id': 'puppet.cortex.delete', 'desc': 'Delete the Puppet ENC configuration', 'detail': system['puppet_certname'] + ' on ' + request.url_root, 'data': system['id'] }) actions.append({ 'id': 'puppet.master.delete', 'desc': 'Delete the system from the Puppet Master', 'detail': system['puppet_certname'] + ' on ' + app.config['PUPPET_MASTER'], 'data': system['puppet_certname'] }) ## Check if TSM backups exist try: tsm_clients = corpus.tsm_get_system(system['name']) #if the TSM client is not decomissioned, then decomission it for client in tsm_clients: if client['DECOMMISSIONED'] is None: actions.append({ 'id': 'tsm.decom', 'desc': 'Decommission the system in TSM', 'detail': 'Node ' + client['NAME'] + ' on server ' + client['SERVER'], 'data': { 'NAME': client['NAME'], 'SERVER': client['SERVER'] } }) except requests.exceptions.HTTPError as e: flash( "Warning - An error occured when communicating with TSM: " + str(ex), "alert-warning") except LookupError: pass except Exception as ex: flash( "Warning - An error occured when communicating with TSM: " + str(ex), "alert-warning") # We need to check all (unique) AD domains as we register development # Linux boxes to the production domain tested_domains = set() for adenv in app.config['WINRPC']: try: # If we've not tested this CortexWindowsRPC host before if app.config['WINRPC'][adenv]['host'] not in tested_domains: # Add it to the set of tested hosts tested_domains.update([app.config['WINRPC'][adenv]['host']]) # If an AD object exists, append an action to delete it from that environment if corpus.windows_computer_object_exists( adenv, system['name']): actions.append({ 'id': 'ad.delete', 'desc': 'Delete the Active Directory computer object', 'detail': system['name'] + ' on domain ' + app.config['WINRPC'][adenv]['domain'], 'data': { 'hostname': system['name'], 'env': adenv } }) except Exception as ex: flash( "Warning - An error occured when communicating with Active Directory: " + str(type(ex)) + " - " + str(ex), "alert-warning") ## Work out the URL for any RHN systems rhnurl = app.config['RHN5_URL'] if not rhnurl.endswith("/"): rhnurl = rhnurl + "/" rhnurl = rhnurl + "rhn/systems/details/Overview.do?sid=" ## Check if a record exists in RHN for this system try: (rhn, key) = corpus.rhn5_connect() rsystems = rhn.system.search.hostname(key, system['name']) if len(rsystems) > 0: for rsys in rsystems: actions.append({ 'id': 'rhn5.delete', 'desc': 'Delete the RHN Satellite object', 'detail': rsys['name'] + ', RHN ID <a target="_blank" href="' + rhnurl + str(rsys['id']) + '">' + str(rsys['id']) + "</a>", 'data': { 'id': rsys['id'] } }) except Exception as ex: flash( "Warning - An error occured when communicating with RHN: " + str(ex), "alert-warning") # If there are actions to be performed, add on an action to raise a ticket to ESM (but not for Sandbox!) if len(actions) > 0 and system['class'] != "play": actions.append({ 'id': 'ticket.ops', 'desc': 'Raises a ticket with operations to perform manual steps, such as removal from monitoring', 'detail': 'Creates a ticket in ServiceNow and assigns it to ' + workflow.config['TICKET_TEAM'], 'data': { 'hostname': system['name'] } }) # Turn the actions list into a signed JSON document via itsdangerous signer = JSONWebSignatureSerializer(app.config['SECRET_KEY']) json_data = signer.dumps(actions) return workflow.render_template("step2.html", actions=actions, system=system, json_data=json_data, title="Decommission Node")
def get(self, host): corpus = Corpus(g.db, app.config) return corpus.dns_lookup(host)
def api_register_system(): """API endpoint for when systems register with Cortex to obtain their Puppet certificates, their Puppet environment, a satellite registration key, etc. Clients can authenticate either via username/password, which is checked against LDAP, or via the VMware virtual machine UUID, which is checked against the VMware systems cache.""" # Create a corpus (task helper) object corpus = Corpus(g.db, app.config) # Clients can send hostname, username and password (interactive installation) if 'hostname' in request.form and 'username' in request.form and 'password' in request.form: # Get the hostname and remove the domain portions, if any # we want the 'short' hostname / the node name hostname = cortex.lib.core.fqdn_strip_domain(request.form['hostname']) # Match the hostname to a system record in the database system = cortex.lib.systems.get_system_by_name(hostname) if not system: app.logger.warn( 'Could not locate host in systems table for register API (hostname: ' + hostname + ')') abort(404) # LDAP username/password authentication if not cortex.lib.user.authenticate(request.form['username'], request.form['password']): app.logger.warn('Incorrect username/password when registering ' + hostname + ', username: '******'username'] + ')') abort(403) # LDAP authorisation if not cortex.lib.user.does_user_have_permission( 'api.register', request.form['username']): app.logger.warn( 'User does not have permission when attempting to register ' + hostname + ', username: '******'username'] + ')') abort(403) interactive = True # OR clients can send the vmware UUID as authentication instead (without a hostname) elif 'uuid' in request.form: # VMware UUID based authentication system = cortex.lib.systems.get_system_by_vmware_uuid( request.form['uuid']) if not system: app.logger.warn( 'Could not match VMware UUID to a system for the register API (UUID given: ' + request.form['uuid'].lower() + ')') abort(404) hostname = system['name'] interactive = False else: app.logger.error( 'Neither UUID or host+username+password sent to authenticate') abort(401) # Increment the build count - this is perhaps useful for tracking rebuilds, # but also means we can generate a new unique password for each register, # which means that a person can't find a VMs original password by running # the register API call again cortex.lib.systems.increment_build_count(system['id']) # Build the node's fqdn fqdn = hostname + '.soton.ac.uk' # Start building the response dictionary cdata = {} # Default to production environment (for CMDB / Satellite) cdata['environment'] = 'Production' # See if the system is linked to ServiceNow and we'll use that # environment. If it isn't linked then we can't do much, so just assume # production (as above) if 'cmdb_environment' in system and system['cmdb_environment']: cdata['environment'] = system['cmdb_environment'] # Get the build identity from the request if 'ident' in request.form: ident = str(request.form['ident']) else: app.logger.warn('No build identity sent when attempting to register ' + hostname) abort(400) # Check that we know about the build identity if ident not in app.config['REGISTER_ACTIONS']: app.logger.warn('Unknown build identity (' + ident + ') sent when attempting to register ' + hostname) abort(400) # See if the build requires Puppet puppet_required = bool('puppet' in app.config['REGISTER_ACTIONS'][ident] and app.config['REGISTER_ACTIONS'][ident]['puppet']) # See if the build requires Satellite satellite_required = bool( 'satellite' in app.config['REGISTER_ACTIONS'][ident] and app.config['REGISTER_ACTIONS'][ident]['satellite']) # See if the build requires a random password password_required = bool( 'password' in app.config['REGISTER_ACTIONS'][ident] and app.config['REGISTER_ACTIONS'][ident]['password']) if puppet_required: # Contact the cortex-puppet-bridge server to get ssl certificates for this hostname autosign_url = app.config['PUPPET_AUTOSIGN_URL'] if not autosign_url.endswith('/'): autosign_url += '/' autosign_url += 'getcert/' + fqdn try: r = requests.get( autosign_url, headers={'X-Auth-Token': app.config['PUPPET_AUTOSIGN_KEY']}, verify=app.config['PUPPET_AUTOSIGN_VERIFY']) except Exception as ex: app.logger.error( "Error occured contacting cortex-puppet-bridge server:" + str(ex)) abort(500) if r.status_code == 200: try: pdata = r.json() except Exception as ex: app.logger.error( "Error occured parsing response from cortex-puppet-bridge server:" + str(ex)) abort(500) for key in ['private_key', 'public_key', 'cert']: if not key in pdata: app.logger.error( "Error occured parsing response from cortex-puppet-bridge server. Parameter '" + key + "' was not sent.") abort(500) cdata[key] = pdata[key] else: app.logger.error( "Error occured contacting cortex-puppet-bridge server. HTTP status code: '" + str(r.status_code) + "'") abort(500) # Systems authenticating by UUID also want to know their hostname and # IP address in order to configure themselves! if not interactive: cdata['hostname'] = system['name'] cdata['fqdn'] = fqdn netaddr = corpus.redis_get_vm_data("ipaddress", uuid=system['vmware_uuid']) netaddrv6 = corpus.redis_get_vm_data("ipv6address", uuid=system['vmware_uuid']) if netaddr: cdata['ipaddress'] = netaddr else: cdata['ipaddress'] = 'dhcp' if netaddrv6: cdata['ipv6address'] = netaddrv6 # Add data for the data and swap disk # (names are reversed here because of Bonemeal) cdata['swap_disk'] = corpus.redis_get_vm_data( "disk_swap", uuid=system['vmware_uuid']) cdata['data_disk'] = corpus.redis_get_vm_data( "disk_data", uuid=system['vmware_uuid']) # Mark as done corpus.redis_set_vm_data("notify", "inprogress", uuid=system['vmware_uuid']) # Add on some other details which could be useful to post-install cdata['user'] = system['allocation_who'] # If the build requires a random password if password_required: # This password is repeatable, but random so long as SECRET_KEY is not compromised cdata['password'] = cortex.lib.systems.generate_repeatable_password( system['id']) # Create puppet ENC entry if it does not already exist if puppet_required: cdata['certname'] = fqdn if 'puppet_certname' in system and system['puppet_certname'] is None: # A system record exists but no puppet_nodes entry. We'll create one! # We set the environment to what we determined above, which defaults # to production, but updates from the CMDB curd = g.db.cursor(mysql.cursors.DictCursor) curd.execute( "INSERT INTO `puppet_nodes` (`id`, `certname`, `env`) VALUES (%s, %s, %s)", (system['id'], fqdn, app.config['PUPPET_DEFAULT_ENVIRONMENT'])) g.db.commit() app.logger.info('Created Puppet ENC entry for certname "' + fqdn + '"') # Get the satellite registration key (if any) if satellite_required: if ident in app.config['SATELLITE_KEYS']: data = app.config['SATELLITE_KEYS'][ident] app.logger.warn("TESTING: USING ENV {} with data {}".format( cdata['environment'], data)) if cdata['environment'] in data: cdata['satellite_activation_key'] = data[cdata['environment']] else: app.logger.warn( 'No Satellite activation key configured for OS ident, ' + str(ident) + ' with environment ' + cdata['environment'] + ' - a Satellite activation key will not be returned') else: app.logger.warn( 'No Satellite activation keys configured for OS ident (' + str(ident) + ') - a Satellite activation key will not be returned') if interactive: cortex.lib.core.log(__name__, "api.register.system", "New system '" + fqdn + "' registered via the API by " + request.form['username'], username=request.form['username']) else: cortex.lib.core.log( __name__, "api.register.system", "New system '" + fqdn + "' registered via the API by VM-UUID authentication") return jsonify(cdata)
def approve(id, status_text=None): """Approves and triggers the build of a system""" sysrequest = get_request_by_id(id) #check if system is already approved if sysrequest['status'] == 2: raise Exception('Request already approved') #load options from request stmt = ('SELECT `workflow`, ' '`hostname`, ' '`sockets`, ' '`cores`, ' '`ram`, ' '`disk`, ' '`template`, ' '`network`, ' '`cluster`, ' '`environment`, ' '`purpose`, ' '`comments`, ' '`expiry_date`, ' '`sendmail` ' 'FROM `system_request` ' 'WHERE `id`=%s') params = (id, ) curd = g.db.cursor(mysql.cursors.DictCursor) curd.execute(stmt, params) results = curd.fetchall()[0] options = {} options['workflow'] = results['workflow'] options['sockets'] = results['sockets'] options['cores'] = results['cores'] options['ram'] = results['ram'] options['disk'] = results['disk'] options['template'] = results['template'] options['network'] = results['network'] options['cluster'] = results['cluster'] options['env'] = results['environment'] options['hostname'] = results['hostname'] options['purpose'] = results['purpose'] options['comments'] = results['comments'] options['expiry'] = results['expiry_date'] options['sendmail'] = results['sendmail'] #trigger build options['wfconfig'] = app.workflows.get('buildvm').config # Connect to NeoCortex and start the task neocortex = cortex.lib.core.neocortex_connect() task_id = neocortex.create_task( 'buildvm', session['username'], options, description="Creates and configures a virtual machine") #update db stmt = 'UPDATE `system_request` SET `status`=2, `updated_at`=NOW(), `updated_who`=%s, `status_text`=%s WHERE `id`=%s' params = (session['username'], status_text, id) curd.execute(stmt, params) g.db.commit() #email requester subject = 'System request approved' message = ( 'Your request for a system has been approved.\n' + '\n' + 'Request id: ' + str(sysrequest['id']) + '\n' + 'Requested at: ' + str(sysrequest['request_date']) + '\n' + 'Reason: ' + str(status_text) + '\n' + '\n' + 'For more details see https://cortex.soton.ac.uk/sysrequest/view/' + str(sysrequest['id'])) corpus = Corpus(g.db, app.config) corpus.send_email(str(sysrequest['requested_who']), subject, message) return redirect(url_for('task_status', id=task_id))
def systems_add_existing(): """Handles the Add Existing System page, which can be used to add missing systems to Cortex""" # Check user permissions if not does_user_have_permission("systems.add_existing"): abort(403) # Get the list of enabled classes classes = cortex.lib.classes.list(hide_disabled=True) # Get the list of Puppet environments puppet_envs = cortex.lib.core.get_puppet_environments() # On GET requests, just show the form if request.method == 'GET': return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # On POST requests... elif request.method == 'POST': # Pull out information we always need if 'class' not in request.form or 'comment' not in request.form or 'env' not in request.form: flash('You must enter all the required information', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") class_name = request.form['class'].strip() comment = request.form['comment'].strip() puppet_env = request.form['env'].strip() # Validate the class if class_name != '_LEGACY' and class_name not in [c['name'] for c in classes]: flash('You must choose a valid class', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # Validate the Puppet environment if len(puppet_env) != 0 and puppet_env not in [e['puppet'] for e in puppet_envs]: flash('You must select either no Puppet environment or a valid Puppet environment', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # Pull out potentially optional information if class_name == '_LEGACY': if 'hostname' not in request.form: flash('You must enter a hostname', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") hostname = request.form['hostname'].strip() if len(hostname) == 0: flash('You must enter a hostname', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") else: if 'number' not in request.form: flash('You must enter a server number', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") number = request.form['number'].strip() # Validate that we have a number if len(number) == 0: flash('You must enter a server number', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") number = int(number) # Get a cursor to the database curd = g.db.cursor(mysql.cursors.DictCursor) # For standard, non-legacy names... if class_name != '_LEGACY': # Get the class curd.execute("SELECT * FROM `classes` WHERE `name` = %s", (class_name,)) class_data = curd.fetchone() # Ensure we have a class if class_data is None: flash('Invalid class selected', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # Generate the name, padded out correctly corpus = Corpus(g.db, app.config) hostname = corpus.pad_system_name(class_name, number, class_data['digits']) # Type 0 (in the database) is a normal class-assigned system system_type = 0 else: # We want NULLs in the database for these for legacy names number = None class_name = None # Type 1 (in the database) is a legacy system system_type = 1 # Insert the system curd.execute("INSERT INTO `systems` (`type`, `class`, `number`, `name`, `allocation_date`, `allocation_who`, `allocation_comment`) VALUES (%s, %s, %s, %s, NOW(), %s, %s)", (system_type, class_name, number, hostname, session['username'], comment)) system_id = curd.lastrowid # Insert a Puppet Node if len(puppet_env) > 0: curd.execute("INSERT INTO `puppet_nodes` (`id`, `certname`, `env`, `include_default`, `classes`, `variables`) VALUES (%s, %s, %s, %s, %s, %s)", (curd.lastrowid, hostname + ".soton.ac.uk", puppet_env, 1, "", "")) # Commit g.db.commit() # If we're linking to VMware if 'link_vmware' in request.form: # Search for a VM with the correct name curd.execute("SELECT `uuid` FROM `vmware_cache_vm` WHERE `name` = %s", (hostname,)) print curd._last_executed vm_results = curd.fetchall() if len(vm_results) == 0: flash("System not linked to VMware: Couldn't find a VM to link the system to", "alert-warning") elif len(vm_results) > 1: flash("System not linked to VMware: Found more than one VM matching the name", "alert-warning") else: curd.execute("UPDATE `systems` SET `vmware_uuid` = %s WHERE `id` = %s", (vm_results[0]['uuid'], system_id)) g.db.commit() # If we're linking to ServiceNow if 'link_servicenow' in request.form: # Search for a CI with the correct name curd.execute("SELECT `sys_id` FROM `sncache_cmdb_ci` WHERE `name` = %s", (hostname,)) print curd._last_executed ci_results = curd.fetchall() if len(ci_results) == 0: flash("System not linked to ServiceNow: Couldn't find a CI to link the system to", "alert-warning") elif len(ci_results) > 1: flash("System not linked to ServiceNow: Found more than one CI matching the name", "alert-warning") else: curd.execute("UPDATE `systems` SET `cmdb_id` = %s WHERE `id` = %s", (ci_results[0]['sys_id'], system_id)) g.db.commit() cortex.lib.core.log(__name__, "systems.add.existing", "System manually added, id " + str(system_id),related_id=system_id) # Redirect to the system page for the system we just added flash("System added", "alert-success") return redirect(url_for('system', id=system_id))
def systems_add_existing(): """Handles the Add Existing System page, which can be used to add missing systems to Cortex""" # Check user permissions if not does_user_have_permission("systems.add_existing"): abort(403) # Get the list of enabled classes classes = cortex.lib.classes.list(hide_disabled=True) # Get the list of Puppet environments puppet_envs = cortex.lib.core.get_puppet_environments() # On GET requests, just show the form if request.method == 'GET': return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # On POST requests... elif request.method == 'POST': # Pull out information we always need if 'class' not in request.form or 'comment' not in request.form or 'env' not in request.form: flash('You must enter all the required information', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") class_name = request.form['class'].strip() comment = request.form['comment'].strip() puppet_env = request.form['env'].strip() # Validate the class if class_name != '_LEGACY' and class_name not in [c['name'] for c in classes]: flash('You must choose a valid class', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # Validate the Puppet environment if len(puppet_env) != 0 and puppet_env not in [e['puppet'] for e in puppet_envs]: flash('You must select either no Puppet environment or a valid Puppet environment', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # Pull out potentially optional information if class_name == '_LEGACY': if 'hostname' not in request.form: flash('You must enter a hostname', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") hostname = request.form['hostname'].strip() if len(hostname) == 0: flash('You must enter a hostname', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") else: if 'number' not in request.form: flash('You must enter a server number', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") number = request.form['number'].strip() # Validate that we have a number if len(number) == 0: flash('You must enter a server number', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") number = int(number) # Get a cursor to the database curd = g.db.cursor(mysql.cursors.DictCursor) # For standard, non-legacy names... if class_name != '_LEGACY': # Get the class curd.execute("SELECT * FROM `classes` WHERE `name` = %s", (class_name,)) class_data = curd.fetchone() # Ensure we have a class if class_data is None: flash('Invalid class selected', category='alert-danger') return render_template('systems/add-existing.html', classes=classes, puppet_envs=puppet_envs, active='systems', title="Add existing system") # Generate the name, padded out correctly corpus = Corpus(g.db, app.config) hostname = corpus.pad_system_name(class_name, number, class_data['digits']) # Type 0 (in the database) is a normal class-assigned system system_type = 0 else: # We want NULLs in the database for these for legacy names number = None class_name = None # Type 1 (in the database) is a legacy system system_type = 1 # Insert the system curd.execute("INSERT INTO `systems` (`type`, `class`, `number`, `name`, `allocation_date`, `allocation_who`, `allocation_comment`) VALUES (%s, %s, %s, %s, NOW(), %s, %s)", (system_type, class_name, number, hostname, session['username'], comment)) system_id = curd.lastrowid # Insert a Puppet Node if len(puppet_env) > 0: curd.execute("INSERT INTO `puppet_nodes` (`id`, `certname`, `env`, `include_default`, `classes`, `variables`) VALUES (%s, %s, %s, %s, %s, %s)", (curd.lastrowid, hostname + ".soton.ac.uk", puppet_env, 1, "", "")) # Commit g.db.commit() # If we're linking to VMware if 'link_vmware' in request.form: # Search for a VM with the correct name curd.execute("SELECT `uuid` FROM `vmware_cache_vm` WHERE `name` = %s", (hostname,)) print curd._last_executed vm_results = curd.fetchall() if len(vm_results) == 0: flash("System not linked to VMware: Couldn't find a VM to link the system to", "alert-warning") elif len(vm_results) > 1: flash("System not linked to VMware: Found more than one VM matching the name", "alert-warning") else: curd.execute("UPDATE `systems` SET `vmware_uuid` = %s WHERE `id` = %s", (vm_results[0]['uuid'], system_id)) g.db.commit() # If we're linking to ServiceNow if 'link_servicenow' in request.form: # Search for a CI with the correct name curd.execute("SELECT `sys_id` FROM `sncache_cmdb_ci` WHERE `name` = %s", (hostname,)) print curd._last_executed ci_results = curd.fetchall() if len(ci_results) == 0: flash("System not linked to ServiceNow: Couldn't find a CI to link the system to", "alert-warning") elif len(ci_results) > 1: flash("System not linked to ServiceNow: Found more than one CI matching the name", "alert-warning") else: curd.execute("UPDATE `systems` SET `cmdb_id` = %s WHERE `id` = %s", (ci_results[0]['sys_id'], system_id)) g.db.commit() # Redirect to the system page for the system we just added flash("System added", "alert-success") return redirect(url_for('system', id=system_id))
def student(): # Get the list of clusters clusters = cortex.lib.core.vmware_list_clusters( workflow.config['STU_VCENTER_TAG']) # Get the list of environments environments = cortex.lib.core.get_cmdb_environments() if request.method == 'GET': # Show form return workflow.render_template( "student.html", title="Create Virtual Machine", os_names=workflow.config['STU_OS_DISP_NAMES'], os_order=workflow.config['STU_OS_ORDER']) elif request.method == 'POST': # Ensure we have all parameters that we require if not {'template', 'network', 'expiry'}.issubset(request.form): flash('You must select options for all questions before creating', 'alert-danger') return redirect(url_for('student')) # Form validation try: try: # Extract all the parameters host_suffix = request.form['hostname'] purpose = request.form['purpose'] comments = request.form['comments'] template = request.form['template'] network = request.form['network'] expiry = request.form['expiry'] sendmail = 'send_mail' in request.form except KeyError as e: flash('Submitted data invalid ' + str(e), 'alert-danger') return redirect(url_for('student')) # Check name is RFC1123 complient if not re.compile(r"^[a-z0-9\-]{1,32}$").match(host_suffix): raise ValueError('Invalid hostname suffix') if not re.compile(r"^[a-z0-9\-]{1,16}$").match( session['username']): raise Exception('Username contains incompatible characters') hostname = 'svm-' + session['username'] + '-' + host_suffix fqdn = hostname + '.ecs.soton.ac.uk' # load corpus corpus = Corpus(g.db, app.config) # ensure name is not in use if corpus.infoblox_get_host_refs(fqdn) is not None: raise ValueError( 'FQDN "' + fqdn + '" appears to have zone records already. Try a different suffix."' ) if template not in workflow.config['STU_OS_ORDER']: raise ValueError('Invalid image selected') if network not in workflow.config['STU_NETWORK_ORDER']: raise ValueError('Invalid network selected') expiry = datetime.datetime.strptime(expiry, '%Y-%m-%d') if expiry < datetime.datetime.utcnow(): raise ValueError('Expiry date cannot be in the past') except ValueError as e: flash(str(e), 'alert-danger') return redirect(url_for('student')) # Build options to pass to the task options = {} options['workflow'] = 'student' options['sockets'] = workflow.config['STU_SPEC_SOCKETS'] options['cores'] = workflow.config['STU_SPEC_CORES'] options['ram'] = workflow.config['STU_SPEC_RAM'] options['disk'] = workflow.config['STU_SPEC_DISK'] options['template'] = template options['network'] = network options['cluster'] = workflow.confg['STU_CLUSTER'] options['env'] = workflow.config['STU_ENV'] options['hostname'] = hostname options['purpose'] = purpose options['comments'] = comments options['expiry'] = expiry options['sendmail'] = sendmail options['wfconfig'] = workflow.config # check if task is running try: curd = g.db.cursor(mysql.cursors.DictCursor) stmt = 'SELECT COUNT(*) AS `count` FROM `tasks` WHERE `username`=%s AND `status`=0' params = (session['username'], ) curd.execute(stmt, params) tasks_in_progress = curd.fetchone()['count'] if tasks_in_progress > 0: flash('You already have a task in progress', 'alert-warning') abort(400) except mysql.Error as e: flash( 'An internal error occurred and your request could not be processed', 'alert-warning') abort(500) # Check if manual approval is required ## They have too many VMs, the VM is to be public facing or is set to expire in over a year or a neocortex task is running # need some way to prevent creation spam where vms are requested faster than they are built if cortex.lib.systems.get_system_count( only_allocated_by=session['username'] ) >= 3 or network == 'external' or expiry > datetime.datetime.utcnow( ) + datetime.timedelta(days=366): try: curd = g.db.cursor(mysql.cursors.DictCursor) sql = 'INSERT INTO `system_request` (`request_date`, `requested_who`, `hostname`, `workflow`, `sockets`, `cores`, `ram`, `disk`, `template`, `network`, `cluster`, `environment`, `purpose`, `comments`, `expiry_date`, `sendmail`, `status`, `updated_at`, `updated_who`) VALUES (NOW(), %s, %s ,%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), %s)' params = (session['username'], options['hostname'], options['workflow'], options['sockets'], options['cores'], options['ram'], options['disk'], options['template'], options['network'], options['cluster'], options['env'], options['purpose'], options['comments'], options['expiry'], options['sendmail'], 0, session['username']) curd.execute(sql, params) g.db.commit() flash( 'Your request could not be completed automatically and is awaiting human approval', 'alert-warning') except mysql.Error as e: flash( 'An internal error occurred and your request could not be processed', 'alert-warning') return redirect(url_for('sysrequests')) try: curd = g.db.cursor(mysql.cursors.DictCursor) sql = 'INSERT INTO `system_request` (`request_date`, `requested_who`, `hostname`, `workflow`, `sockets`, `cores`, `ram`, `disk`, `template`, `network`, `cluster`, `environment`, `purpose`, `comments`, `expiry_date`, `sendmail`, `status`, `updated_at`, `updated_who`) VALUES (NOW(), %s, %s ,%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), %s)' params = (session['username'], options['hostname'], options['workflow'], options['sockets'], options['cores'], options['ram'], options['disk'], options['template'], options['network'], options['cluster'], options['env'], options['purpose'], options['comments'], options['expiry'], options['sendmail'], 2, session['username']) curd.execute(sql, params) g.db.commit() flash('Your request has been automatically approved', 'alert-success') except mysql.Error as e: flash( 'An internal error occurred and your request could not be processed', 'alert-warning') # Connect to NeoCortex and start the task neocortex = cortex.lib.core.neocortex_connect() task_id = neocortex.create_task( __name__, session['username'], options, description="Creates and configures a virtual machine") # Redirect to the status page for the task return redirect(url_for('task_status', id=task_id))