#!/usr/bin/python2.7 # # Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. # """svcbundle is a program to generate SMF manifests. svcbundle accepts multiple occurrences of -s name=value. Legal values of name are: Name Value ---- ----- bundle-type Type of service bundle to generate. Legal values are "manifest" and "profile". manifest is the default. day The day parameter of a scheduled service (see svc.periodicd(1M)). day_of_month The day_of_month parameter of a scheduled service (see svc.periodicd(1M)). duration Synonym for model. enabled Indicates whether or not the instance should be enabled. Legal values are "true" and "false". The default value is true. frequency The frequency parameter of a scheduled service (see svc.periodicd(1M)). hour The hour parameter of a scheduled service (see svc.periodicd(1M)). interval The interval parameter of a scheduled service (see svc.periodicd(1M)). This NV pair, when combined with the start-method pair, will cause svcbundle to emit a manifest for a scheduled service. minute The minute parameter of a scheduled service (see svc.periodicd(1M)). model Sets the service model. This is the value of the startd/duration property. Refer to svc.startd(1M). Model can be set to one of the following values: contract daemon - synonym for contract child wait - synonym for child transient The default is transient. month The month parameter of a scheduled service (see svc.periodicd(1M)). instance-name Name of the instance. The default value is "default". instance-property=pg_name:prop_name:prop_type:value service-property=pg_name:prop_name:prop_type:value These options are used to create a property group named pg_name in the instance or service, and it will have a type of application. The PG will have a single property named prop_name with a single value that is of type prop_type. Property groups with multiple properties can be created by invoking *-property multiple times. Zero or more *-property= declarations can be used. The property type can be defaulted by using two consecutive colons. See example 7. For manifests a default property type of astring will be used. Profiles do not require that the property be specified, since it can usually be determined from other sources of information. period The period of a periodic service (see svc.periodicd(1M)). This NV pair, when combined with the start-method pair, will cause svcbundle to emit a manifest for a periodic service. rc-script=script_path:run_level This NV pair causes svcbundle to emit a manifest that facilitates conversion of a legacy rc script to an SMF service. script_path is the path to the rc script and run_level is the run level (see init(1M)) where the rc script runs. script_path is used to generate the start and stop exec_method elements in the manifest. The exec attribute will be set to script_path %m run_level is used to generate depencencies, so that the script runs at the appropriate time during booting. service-name Name of the service. This NV pair is required. start-method The command to execute when the service is started. White space is allowed in the value. The method tokens that are introduced by % as documented in smf_method(5) are allowed and will be placed in the manifest for expansion by the restarter. ":true" is allowed. This NV pair is required for manifests unless the rc-script NV pair is specified. It is not required for profiles. stop-method The command to execute when the service is stopped. It accepts values like start-method and also accepts ":kill". :true is the default value for transient services and :kill for contract and child services. timezone The timezone parameter of a scheduled service (see svc.periodicd(1M)). weekday_of_month The weekday_of_month parameter of a scheduled service (see svc.periodicd(1M)). week_of_year The week_of_year parameter of a scheduled service (see svc.periodicd(1M)). year The year parameter of a scheduled service (see svc.periodicd(1M)). There can be multiple occurrences of instance-property and service-property, but the rest of the names can occur only once. The command line parameters will be represented as a dictionary where the name is the key. The values for instance-property and service-property will be represented as a CLProperties object, since there can be multiple occurrences of these names. """ try: import calendar from collections import namedtuple import errno from gettext import gettext from optparse import OptionParser, SUPPRESS_USAGE import os import re import socket import string import subprocess import sys import textwrap import time import warnings from xml.dom.minidom import getDOMImplementation from xml.dom.minidom import Node except KeyboardInterrupt: # Avoid obnoxious Python traceback on interrupt. import sys sys.exit(1) # Turn python warnings into errors, so that we get tracebacks: warnings.simplefilter("error") # Declare strings for the command line names: NM_BUNDLE_TYPE = "bundle-type" NM_DAY = "day" NM_DAY_OF_MONTH = "day_of_month" NM_DURATION = "duration" NM_ENABLED = "enabled" NM_FREQUENCY = "frequency" NM_HOUR = "hour" NM_INST_NAME = "instance-name" NM_INST_PROP = "instance-property" NM_INTERVAL = "interval" NM_MINUTE = "minute" NM_MODEL = "model" NM_MONTH = "month" NM_PERIOD = "period" NM_RC_SCRIPT = "rc-script" NM_REFRESH = "refresh-method" NM_START = "start-method" NM_STOP = "stop-method" NM_SVC_NAME = "service-name" NM_SVC_PROP = "service-property" NM_TIMEZONE = "timezone" NM_WEEK_OF_YEAR = "week_of_year" NM_WEEKDAY_OF_MONTH = "weekday_of_month" NM_YEAR = "year" # NM_OUTPUT_FILE is not an actual name on the command line. It is set by # by -o or -i. NM_OUTPUT_FILE = "output-file" def check_hostname(host): length = len(host) if (length > ValueType.SCF_LIMIT_MAX_VALUE_LENGTH) or (length > 255): raise PropValueError(gettext("Host name is too long")) if host[-1:] == ".": # Strip one period from right side host = host[:-1] for dom in host.split("."): if not check_name(NVPair.comp_domain_re, dom): raise PropValueError( gettext( "\"{0}\" is an invalid hostname component".format(dom))) def check_ip_addr_by_type(addr_type, addr): """Verify that addr is a valid IP address of the addr_type specified.""" if addr_type not in [socket.AF_INET, socket.AF_INET6]: raise SvcbundleError(gettext("Invalid network type")) comp = addr.split("/", 1) if (len(comp) > 1) and (len(comp[1]) > 0): # We have a net mask designator. try: bits = long(comp[1]) except ValueError: raise PropValueError(gettext("Invalid net mask specifier")) if addr_type == socket.AF_INET: maxbits = 32 else: maxbits = 128 if (bits < 0) or (bits > maxbits): raise PropValueError(gettext("Invalid net mask specifier")) if len(comp[0]) == 0: raise PropValueError(gettext("Network address has 0 length")) try: socket.inet_pton(addr_type, comp[0]) except socket.error: raise PropValueError(gettext("Network address is invalid")) def check_ip_addr(addr): """Verify that addr is either a v4 or v6 IP address.""" for addr_type in [socket.AF_INET, socket.AF_INET6]: try: check_ip_addr_by_type(addr_type, addr) return except PropValueError: continue else: raise PropValueError(gettext("Value is an invalid IP address")) def check_name(cre, name): """Return True if name matches the regular expression. cre is a compiled regular expression. This function checks to see if the re covers all the characters in name. check_name() returns True if the re does. """ mo = cre.match(name) if mo is None: return False else: return mo.group() == name def check_utf8(str): """Verify that str is a valid utf8 string.""" try: str.decode("utf-8", "strict") except UnicodeError: raise PropValueError(gettext("Invalid UTF-8 string")) def extract_pg_info(parent, params, key): """Return a list of the property group information at key in params. The params entry at key is a CLProperties object which contains the information from all of *-properties for either instance or service. From the CLProperties object we get a list of property group trees. For each PG in the list we create a SmfPropertyGroup object and then form a tuple consisiting of the SmfPropertyGroup object and the property group tree. We return a list of these tuples. """ rv = [] properties = params.get(key) if properties == None: return rv pg_list = properties.get_property_groups() for pg_tree in pg_list: pg = SmfPropertyGroup(parent) rv.append((pg, pg_tree)) return rv def is_manifest(): """Return True if we are generating a manifest.""" return SmfNode.is_manifest def safe_so(*text): """Send text to stdout while handling pipe breakage.""" try: print ' '.join([str(s) for s in text]) except IOError as e: if e.errno == errno.EPIPE: return raise def safe_se(*text): """Send text to stderr while handling pipe breakage.""" try: sys.stderr.write(' '.join([str(s) for s in text])) except IOError as e: if e.errno == errno.EPIPE: return raise def fmri_extract_scope(fmri): """Extract the scope from the fmri. Returns a tuple consisting of (scope, remainder of FMI). """ if fmri.startswith(ValueType.SCF_FMRI_SCOPE_PREFIX): # Remove the scope prefix. fmri = fmri[len(ValueType.SCF_FMRI_SCOPE_PREFIX):] # The service prefix (/) acts as the terminator of the scope. parts = fmri.split(ValueType.SCF_FMRI_SERVICE_PREFIX, 1) scope = parts[0] if len(parts) > 1: # Found the service prefix. The text after the prefix is the # remainder of the FMRI. fmri = parts[1] else: # The entire fmri is the scope. Nothing remains after it. fmri = "" # If the scope ends with the scope suffix, remove it. index = scope.rfind(ValueType.SCF_FMRI_SCOPE_SUFFIX) if index >= 0: scope = scope[0:index] else: scope = None # Bypass the service prefix if it exists. if fmri.startswith(ValueType.SCF_FMRI_SERVICE_PREFIX): fmri = fmri[len(ValueType.SCF_FMRI_SERVICE_PREFIX):] return (scope, fmri) def fmri_extract_service_instance(fmri): """Extract the service and instance part of fmri. Returns a tuple consisting of (service, instance, remainder). The remainder will be positioned just past the property group prefix if it exists or at the end of the string if it doesn't. """ svc = None inst = None # Extract the service. It can be terminated by instance prefix, # property group prefix (if there is no instance specification) or # the end of the string. inst_idx = fmri.find(ValueType.SCF_FMRI_INSTANCE_PREFIX) pg_idx = fmri.find(ValueType.SCF_FMRI_PROPERTYGRP_PREFIX) if inst_idx >= 0: if pg_idx >= 0: if inst_idx < pg_idx: # We have an instance. Service ends at inst. prefix. svc = fmri[0:inst_idx] # Instance ends at property group prefix. inst = fmri[ inst_idx + len(ValueType.SCF_FMRI_INSTANCE_PREFIX):pg_idx] else: # No instance. We just found the : in the PF prefix. # Service ends at PG prefix. svc = fmri[0:pg_idx] # Remainder is everthing past the property group prefix. fmri = fmri[ pg_idx + len(ValueType.SCF_FMRI_PROPERTYGRP_PREFIX):] else: # We have an instance, but no PG. svc = fmri[0:inst_idx] inst = fmri[inst_idx + len(ValueType.SCF_FMRI_INSTANCE_PREFIX):] fmri = "" else: # No instance. if pg_idx >= 0: # We have a PG. Service ends at PG prefix. svc = fmri[0:pg_idx] # Remainder is everthing past the property group prefix. fmri = fmri[ pg_idx + len(ValueType.SCF_FMRI_PROPERTYGRP_PREFIX):] else: # No instance or PG. svc = fmri fmri = "" return (svc, inst, fmri) def validate_svc_fmri(fmri): """Validate the supplied service FMRI. This function is based on scf_parse_svc_fmri() in lowlevel.c. """ svc = None inst = None pg = None prop = None if len(fmri) > ValueType.SCF_LIMIT_MAX_VALUE_LENGTH: raise PropValueError(gettext("FMRI is too long")) # Skip service prefix (svc:) if it exists. if fmri.startswith(ValueType.SCF_FMRI_SVC_PREFIX): fmri = fmri[len(ValueType.SCF_FMRI_SVC_PREFIX):] scope, fmri = fmri_extract_scope(fmri) svc, inst, fmri = fmri_extract_service_instance(fmri) # If fmri is not empty, it now starts with the PG name. if len(fmri) > 0: parts = fmri.split(ValueType.SCF_FMRI_PROPERTY_PREFIX, 1) pg = parts[0] if len(parts) > 1: prop = parts[1] # Now do the validations: if (scope is not None) and (len(scope) > 0): if not check_name(NVPair.comp_ident_re, scope): raise PropValueError( gettext("FMRI contains invalid scope")) if (svc is None) or (len(svc) == 0): raise PropValueError( gettext("Service name is empty in FMRI")) else: if not check_name(NVPair.comp_service_re, svc): raise PropValueError( gettext("Illegal service name in FMRI")) if inst is not None: if len(inst) == 0: raise PropValueError( gettext("Instance name is empty in FMRI")) if not check_name(NVPair.comp_name_re, inst): raise PropValueError( gettext("Illegal instance name in FMRI")) if pg is not None: if len(pg) == 0: raise PropValueError( gettext("Property group name is empty in FMRI")) if not check_name(NVPair.comp_name_re, pg): raise PropValueError( gettext("Illegal property group name in FMRI")) if prop is not None: if len(prop) == 0: raise PropValueError( gettext("Property name is empty in FMRI")) if not check_name(NVPair.comp_name_re, prop): raise PropValueError( gettext("Illegal property name in FMRI")) def validate_file_fmri(fmri): """Validate a file type fmri. This function is based on scf_parse_file_fmri() in lowlevel.c. """ if fmri.startswith(ValueType.SCF_FMRI_FILE_PREFIX): fmri = fmri[len(ValueType.SCF_FMRI_FILE_PREFIX):] if fmri.startswith(ValueType.SCF_FMRI_SCOPE_PREFIX): fmri = fmri[len(ValueType.SCF_FMRI_SCOPE_PREFIX):] scope_end = fmri.find("/") if scope_end >= 0: scope = fmri[0:scope_end] fmri = fmri[scope_end:] else: scope = fmri if (len(scope) > 0) and (scope != ValueType.SCF_FMRI_LOCAL_SCOPE): raise PropValueError( gettext("FMRI contains invalid scope")) else: # Path must be absolute if not fmri.startswith("/"): raise PropValueError( gettext("File FMRI paths must be absolute")) """ We define the following base classes for use by svcbundle: PropDef - is a namedtuple holding the information from command line names instance-property and service-property. RcDef - is a namedtuple holding the from the rc-script command line name. SmfNode - Represents SMF object in the Document Object Model (DOM) representation of a manifest or profile. NVPair - Represents the name=value parameters of the -s option on the command line. SvcbundleError - Base class for exceptions for internal use. """ # PropDef holds the property group name, property name, property type and # value for a property. PropDef = namedtuple("PropDef", "pg_name prop_name prop_type value") # RcDef holds the rc script path and run level from the rc-script name. RcDef = namedtuple("RcDef", "script run_level") class SvcbundleError(Exception): """Base class for program specific exceptions""" class CLError(SvcbundleError): """Errors on the command line""" class PropValueError(SvcbundleError): """Property value is invalid""" class Tree(object): def __init__(self, value): self.value = value self.branches = [] def __getitem__(self, index): """Index through the branches.""" return self.branches[index] def __len__(self): return len(self.branches) def find_or_add(self, value): for branch in self.branches: if branch.value == value: return branch branch = Tree(value) self.branches.append(branch) return branch class CLProperties(object): """Store property definitions in a tree structure. PropDefs which contain PG name, property name, property type and value are converted and stored in a tree structure. This allows us to detect declaration of multi-valued properties and PGs with multiple properties. The top tier of the tree contains PG names, the second layer property names, the third layer property types and the fourth layer contains the property values. The property value nodes have no branches. """ # UNSPECIFIED_PROP_TYPE is used when the user does not specify a # property type. DEFAULT_PROP_TYPE is the default property type when # none is specified. DEFAULT_PROP_TYPE = "astring" UNSPECIFIED_PROP_TYPE = "default" def __init__(self): self.root = Tree(None) def __getitem__(self, index): """Return the tree in our branches at the specified index.""" return self.root.branches[index] def add_propdef(self, pd): pg = self.root.find_or_add(pd.pg_name) prop = pg.find_or_add(pd.prop_name) type_tree = prop.find_or_add(pd.prop_type) type_tree.find_or_add(pd.value) def get_property_groups(self): """Return a list of property group trees.""" return self.root.branches # Classes that encapsulate DOM objects: class SmfNode(object): """ Each SmfNode encapsulates a Node object from minidom. From a programming point of view, it would have been nice to just inherit directly from Node. Unfortunately, most Node objects come into being by calling create* functions, and these functions would bypass our constructors. Thus, we use the minidom Node objects to hold the DOM tree structure and the SmfNode objects to understand the details of the SMF DTD. Each SmfNode references a minidom object in the node attribute. We also add an attribute, smf_node, to each Node that points to the corresponding SmfNode object. The SmfNode objects use the following attributes: node - corresponding minidom Node object parent - minidom Node object that is the parent of node The SmfNode objects have the following methods: intro_comment - provides a comment to be displayed as an introduction to the element in the generated manifest. propagate - driven by the command line parameters, create the offspring SmfNodes translate_attribute - Intended to be overriden by classes that output values to handle translation of special characters to entities. writexml - The xml that is generated by the minidom module is ugly in that it does not handle long lines well. Thus, we provide our own writexml method to produce more readable manifests. We provide a few class attributes to provide easy access throughout the program. document - Document node. This is the root of the document. implementation - DOM implementation installing - A boolean to indicate that -i was on the command line. is_manifest - True if we are creating a manifest. method_timeout - Default timeout for exec_methods. parser - The object that is used to parse the command line. We keep it here to give easy access to parser.error() for generation error messages. pname - Program name for use in error messages. wrapper - A TextWrapper object to beautify long lines of output. """ document = None implementation = None installing = False is_manifest = True method_timeout = "60" parser = None pname = "svcbundle" wrapper = textwrap.TextWrapper( width=75, break_long_words=False, break_on_hyphens=False) def __init__(self, parent, element_type): """Create a DOM element and setup linkages. Parameters: parent - minidom Node that is the parent of the created node element_type - string specify the element type A new minidom element is created and appeneded to parent. The node is saved in our node attribute and an smf_node attribute is added to node to point to us. """ self.parent = parent node = self.document.createElement(element_type) node.smf_node = self self.node = node parent.appendChild(node) def intro_comment(self, text): """Create a comment to introduce this element in the manifest. A comment node is created containing text, and it is inserted in front of our node in the DOM. """ SmfComment(self.parent, text, self.node) def translate_attribute(self, name, value): return value def writexml(self, writer, indent="", addindent="", newl=""): """Write the XML for this node and its descandents. Parameters: writer - Output channel indent - String preceed the output of this node addindent - Additional indentation for descendant nodes newl - String to be output at the end of a line Output the opening XML tag of this node along with any attributes. Descend into any child elements to write them out and then write the closing tag. We can't use wrapper here, because an attribute value might have white space in it. In this case the TextWrapper code might break the line at that white space. """ node = self.node assert node.nodeType == node.ELEMENT_NODE width = self.wrapper.width line = "{0}<{1}".format(indent, node.tagName) attr_indent = indent + addindent attributes = node.attributes if attributes is not None: for i in range(attributes.length): att = attributes.item(i) node_value = att.nodeValue # Convert special XML characters to character entities node_value = self.translate_attribute(att.nodeName, node_value) next_attr = "{0}=\"{1}\"".format( att.nodeName, node_value) if len(line) + len(next_attr) + 1 > width: # Need to write the current line before this attribute writer.write("{0}{1}".format(line, newl)) line = attr_indent + next_attr else: line += " " + next_attr if node.hasChildNodes(): terminator = ">" else: terminator = "/>" if len(line) + len(terminator) > width: # Write the line before adding the terminator writer.write("{0}{1}".format(line, newl)) line = indent writer.write("{0}{1}{2}".format(line, terminator, newl)) # # Process the children # if node.hasChildNodes(): # Write the children for n in node.childNodes: n.smf_node.writexml( writer, indent + addindent, addindent, newl) # Write the closing tag writer.write("{0}{2}".format( indent, node.tagName, newl)) class SmfComment(SmfNode): """ This class encapsulates a DOM Comment node""" def __init__(self, parent, text, before_me=None): """Create an SmfComment containing text. If before_me is None, the comment node will be appended to parent. Otherwise, before_me must be a node in parent, and the comment node will be inserted in front of before_me. """ com = self.document.createComment(text) com.smf_node = self self.node = com if (before_me is None): parent.appendChild(com) else: parent.insertBefore(com, before_me) def writexml(self, writer, indent="", addindent="", newl=""): """Write an XML comment on writer. Parameters: writer - Output channel indent - String preceed the output of this node addindent - Additional indentation for descendant nodes newl - String to be output at the end of a line Print the XML opening comment delimiter. Then output the text of the comment with addindent additional indentation wrapping the text at column 75. Finally, write the XML closing comment delimiter on a line by itself. """ node = self.node if "--" in node.data: raise ValueError("'--' is not allowed in a comment node.") assert node.nodeType == Node.COMMENT_NODE writer.write("{0}{2}".format(newl, indent, newl)) class SmfDocument(SmfNode): """This class encapsulates a DOM Document node. See http://docs.python.org/release/2.7/library/xml.dom.html for details of the parameters. """ def __init__(self, qualifiedName, doc_type): doc = self.implementation.createDocument( None, qualifiedName, doc_type.node) self.node = doc doc.smf_node = self def writexml(self, writer, indent="", addindent="", newl=""): writer.write('' + newl) for node in self.node.childNodes: node.smf_node.writexml(writer, indent, addindent, newl) class SmfDocumentType(SmfNode): """This class encapsulates a DOM DocumentType node. See http://docs.python.org/release/2.7/library/xml.dom.html for details of the parameters. """ def __init__(self): doc_type = self.implementation.createDocumentType("service_bundle", "", "/usr/share/lib/xml/dtd/service_bundle.dtd.1") doc_type.smf_node = self self.node = doc_type def writexml(self, writer, indent="", addindent="", newl=""): self.node.writexml(writer, indent, addindent, newl) # Classes that represent parts of the SMF DTD: class SmfBundle(SmfNode): """Implement the service_bundle element from the DTD""" def __init__(self, parent, element): """Initialize an SmfBundle. SmfBundle is unique in that the corresponding DOM element is created automatically by createDocument(). Thus, the element is passed in as a parameter rather than being created here. """ self.parent = parent element.smf_node = self self.node = element def propagate(self, params): """Add our descendants to the DOM. Add our attributes and a service to the DOM. Then ask the service to propagate itself. """ bundle = self.node # First set our attributes. We set name from the service name and # type from the command line parameters. The default type is # manifest. name = params.get(NM_SVC_NAME, None) if name is None: raise CLError(gettext("No {0} specified.").format(NM_SVC_NAME)) bundle.setAttribute("name", name) bundle_type = params.get(NM_BUNDLE_TYPE, "manifest") bundle.setAttribute("type", bundle_type) # Since we have a service name, create the service and tell it to # propagate. service = SmfService(bundle) service.propagate(params) class SmfDependency(SmfNode): """Base class for generating dependencies.""" # Inheriting classes are permitted to override these variables. _grouping = "require_all" _restart_on = "none" def __init__(self, parent, name, fmri): """Create a dependency node. Parameters are: parent - Parent for this node in the DOM. name - Name of the dependency. fmri - FMRI to be dependent upon. """ SmfNode.__init__(self, parent, "dependency") self.fmri = fmri self.dependency_name = name def propagate(self, params): """Generate the dependency in the DOM.""" # First set the attributes -- name, gouping, restart_on and type. dep = self.node dep.setAttribute("name", self.dependency_name) dep.setAttribute("grouping", self._grouping) dep.setAttribute("restart_on", self._restart_on) dep.setAttribute("type", "service") # Create the service_fmri for the service that we are dependent # upon. svc_fmri = SmfServiceFmri(dep, self.fmri) svc_fmri.propagate(params) class SmfAutoDependency (SmfDependency): """Generate the automatic dependency on svc:/milestone/multi-user.""" def __init__(self, parent): SmfDependency.__init__( self, parent, "multi_user_dependency", "svc:/milestone/multi-user") def propagate(self, params): """Add an explanatory comment and our attributes to the DOM. The depencency is hard coded. There are no command line parameters to alter it. """ self.intro_comment(gettext( "The following dependency keeps us from starting " "until the multi-user milestone is reached.")) SmfDependency.propagate(self, params) class SmfSingleUserDependency(SmfDependency): def __init__(self, parent): SmfDependency.__init__( self, parent, "single_user_dependency", "svc:/milestone/single-user") class SmfMultiUserDependency(SmfDependency): def __init__(self, parent): SmfDependency.__init__( self, parent, "multi_user_dependency", "svc:/milestone/multi-user") class SmfMultiUserServerDependency(SmfDependency): def __init__(self, parent): SmfDependency.__init__( self, parent, "multi_user_server_dependency", "svc:/milestone/multi-user-server") class SmfDependent(SmfNode): """Base class for dependent elements.""" # Inheriting classes are permitted to override these variables. _grouping = "optional_all" _restart_on = "none" def __init__(self, parent, name, fmri): """Create a dependent node. Parameters are: parent - Parent for this node in the DOM. name - Name of the dependent. fmri - FMRI of the service that depends on us. """ SmfNode.__init__(self, parent, "dependent") self.fmri = fmri self.dependent_name = name def propagate(self, params): dep = self.node dep.setAttribute("name", self.dependent_name) dep.setAttribute("grouping", self._grouping) dep.setAttribute("restart_on", self._restart_on) # Create the service_fmri for the service that depends on us. svc_fmri = SmfServiceFmri(dep, self.fmri) svc_fmri.propagate(params) class SmfMultiUserDependent(SmfDependent): def __init__(self, parent): SmfDependent.__init__( self, parent, "multiuser_dependent", "svc:/milestone/multi-user") class SmfMultiUserServerDependent(SmfDependent): def __init__(self, parent): SmfDependent.__init__( self, parent, "multiuser_server_dependent", "svc:/milestone/multi-user-server") class SmfExecMethod(SmfNode): """Base class for the start, stop and refresh exec_method elements""" def __init__(self, parent, name): """Create an exec_method element in the DOM. name which is passed in by one of the overriding classes is the name of the exec method. """ SmfNode.__init__(self, parent, "exec_method") self.method_name = name def propagate(self, params): """Add our attributes to the node.""" em = self.node em.setAttribute("name", self.method_name) em.setAttribute("type", "method") em.setAttribute("timeout_seconds", self.method_timeout) class SmfPeriodicMethod(SmfNode): """Base class for periodic method elements""" def __init__(self, parent): """Create a periodic_method element in the DOM.""" SmfNode.__init__(self, parent, "periodic_method") def propagate(self, params): """Add our attributes to the DOM.""" em = self.node em.setAttribute("delay", "0") em.setAttribute("jitter", "0") em.setAttribute("recover", "false") em.setAttribute("persistent", "false") em.setAttribute("timeout_seconds", "0") start = params.get(NM_START) if (start is None): raise CLError(gettext("{0} is required").format(NM_START)) em.setAttribute("exec", start) period = params.get(NM_PERIOD) if (period is None): raise CLError(gettext("{0} is required").format(NM_PERIOD)) em.setAttribute("period", period) class SmfScheduledMethod(SmfNode): """Base class for scheduled method elements""" def __init__(self, parent): """Create a scheduled_method element in the DOM.""" SmfNode.__init__(self, parent, "scheduled_method") def propagate_optional_param(self, name): """Add optional attributes to this element""" em = self.node val = self.my_params.get(name) if (val is not None): em.setAttribute(name, val) def propagate(self, params): """Add our attributes to the DOM.""" self.my_params = params em = self.node em.setAttribute("recover", "false") start = params.get(NM_START) if (start is None): raise CLError(gettext("{0} is required").format(NM_START)) em.setAttribute("exec", start) interval = params.get(NM_INTERVAL) if (interval is None): raise CLError(gettext("{0} is required").format(NM_INTERVAL)) em.setAttribute("interval", interval) exclusive = [(NM_MONTH, (NM_WEEK_OF_YEAR)), (NM_WEEK_OF_YEAR, (NM_MONTH, NM_DAY_OF_MONTH, NM_WEEKDAY_OF_MONTH)), (NM_DAY, (NM_DAY_OF_MONTH))] # make sure that there aren't any conflicting attributes for (key, excluded) in exclusive: if (params.get(key) is not None): for excl in excluded: if (params.get(excl) is not None): raise CLError(gettext("{0} cannot be used with {1}"). format(key, excl)) self.propagate_optional_param(NM_FREQUENCY) self.propagate_optional_param(NM_TIMEZONE) self.propagate_optional_param(NM_YEAR) self.propagate_optional_param(NM_WEEK_OF_YEAR) self.propagate_optional_param(NM_MONTH) self.propagate_optional_param(NM_DAY_OF_MONTH) self.propagate_optional_param(NM_WEEKDAY_OF_MONTH) self.propagate_optional_param(NM_DAY) self.propagate_optional_param(NM_HOUR) self.propagate_optional_param(NM_MINUTE) em.setAttribute("timeout_seconds", "0") class SmfExecRcMethod(SmfExecMethod): """Base class for rc script start and stop methods""" def __init__(self, parent, name): SmfExecMethod.__init__(self, parent, name) def propagate(self, params): """Add our attributes to the DOM. Use our base class to generate the standard attributes. Then we'll set the exec method using the path to the rc script. """ SmfExecMethod.propagate(self, params) em = self.node rc_params = params.get(NM_RC_SCRIPT) assert rc_params is not None em.setAttribute("exec", "{0} %m".format(rc_params.script)) class SmfExecMethodRcStart(SmfExecRcMethod): """Implement start exec_method for rc scripts.""" def __init__(self, parent): SmfExecRcMethod.__init__(self, parent, "start") class SmfExecMethodRcStop(SmfExecRcMethod): """Implement stop exec_method for rc scripts.""" def __init__(self, parent): SmfExecRcMethod.__init__(self, parent, "stop") class SmfExecMethodRefresh(SmfExecMethod): """Implements the refresh exec_method element.""" def __init__(self, parent): SmfExecMethod.__init__(self, parent, "refresh") def propagate(self, params): """Generate attribute nodes and an introductory comment. Use our base class to generate the standard attributes. Then handle the exec attribute ourself. If a refresh method was specified on the command line, we'll use that as our exec attribute. Otherwise, we generate a comment explaining the exec attribute and generate an exec attribute of :true. """ SmfExecMethod.propagate(self, params) em = self.node refresh = params.get(NM_REFRESH) if (refresh is not None): em.setAttribute("exec", refresh) else: self.intro_comment( gettext( "The exec attribute below can be changed to a " "command that SMF should execute when the service is " "refreshed. Services are typically refreshed when " "their properties are changed in the SMF repository. " "See smf_method(5) for more details. It is common " "to retain the value of :true which means that SMF " "will take no action when the service is refreshed. " "Alternatively, you may wish to provide a method to " "reread the SMF repository and act on any " "configuration changes.")) em.setAttribute("exec", ":true") class SmfExecMethodStart(SmfExecMethod): """Implements the start exec_method element.""" def __init__(self, parent): SmfExecMethod.__init__(self, parent, "start") def propagate(self, params): """Add our attributes to the DOM. Use our base class to generate the standard attributes. Then handle the exec attribute ourself. The start-method is required to be specified. """ SmfExecMethod.propagate(self, params) em = self.node start = params.get(NM_START) if (start is None): raise CLError(gettext("{0} is required").format(NM_START)) em.setAttribute("exec", start) class SmfExecMethodStop(SmfExecMethod): """Implements the stop exec_method element.""" def __init__(self, parent): SmfExecMethod.__init__(self, parent, "stop") def propagate(self, params): """Add our attributes to the DOM. Use our base class to generate the standard attributes. Then handle the exec attribute ourself. If a stop method was specified on the command line, we'll use that as our exec attribute. Otherwise, we generate a comment explaining the exec attribute and generate an exec attribute of :true for transient duration and :kill for other durations. """ SmfExecMethod.propagate(self, params) em = self.node # If we have a stop method, go ahead and use it. Otherwise, emit a # comment explaining what to do with the exec method. stop = params.get(NM_STOP, None) if (stop is not None): em.setAttribute("exec", stop) else: self.intro_comment( gettext( "The exec attribute below can be changed to a " "command that SMF should execute to stop the " "service. See smf_method(5) for more details.")) duration = params.get(NM_DURATION, "transient") if duration == "transient": # Transient services cannot be :killed. stop = ":true" else: stop = ":kill" em.setAttribute("exec", stop) class SmfLoctext(SmfNode): """Implements the loctext element of the DTD.""" def __init__(self, parent): SmfNode.__init__(self, parent, "loctext") def propagate(self, text): """text will be placed as a comment in the loctext element.""" node = self.node node.setAttribute("xml:lang", "C") comment = SmfComment(node, text) class SmfInstance(SmfNode): """Implements the instance element of the DTD.""" def __init__(self, parent): SmfNode.__init__(self, parent, "instance") def propagate(self, params): """Set our attributes. Generate and propagate our children. Create the name and enabled attributes in the DOM. Additional subelements that are added to the DOM and propagated are: dependency start, stop and refresh methods duration property group Then we create and propagate any property groups that were specified on the command line. """ manifest = is_manifest() # First set our attributes inst = self.node name = params.get(NM_INST_NAME, "default") inst.setAttribute("name", name) # If we are creating a profile and if -i (installing) was # specified, make sure that the instance already exists. If it # doesn't exist the user is trying to create an instance via a # profile, but there will be no complete attribute. This will most # likely be frustrating, because svcs will not show the instance # without the complete attribute. The complete attribute is an # advanced concept and not supported by this program. if (not manifest) and SmfNode.installing: fmri = "svc:/{0}:{1}".format(params.get(NM_SVC_NAME), name) cmd = ["/usr/bin/svcs", "-H", fmri] try: inst_check = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: SmfNode.parser.error( gettext( "Unable to run svcs. {0}.").format( os.strerror(e.errno))) if inst_check.returncode != 0: SmfNode.parser.error( gettext( "Service instance {0} does not exist on this " "system, and you are trying to create it with the " "generated profile. {1} does not support instance " "creation with profiles, so you should use a " "manifest instead.").format(fmri, SmfNode.pname)) # For manifests we generate the enabled attribute with a default # value of true if it was not specified on the command line. For # profiles we are not required to set the enabled attribute. enabled = params.get(NM_ENABLED) if enabled is None: if manifest: inst.setAttribute("enabled", "true") else: inst.setAttribute("enabled", enabled) # Propagate the property groups: pg_propagate = extract_pg_info(inst, params, NM_INST_PROP) for pg, pg_def in pg_propagate: pg.propagate(pg_def) class SmfValueNode(SmfNode): """Implement value_node element.""" def __init__(self, parent, property_type): self.property_type = property_type SmfNode.__init__(self, parent, "value_node") def translate_attribute(self, name, value): # The only attribute that we set is the value attribute. assert name == "value" return ValueType.translate_output(self.property_type, value) def propagate(self, value): self.node.setAttribute("value", value) class ValueList(SmfNode): """Base class for value lists""" _list_type = None def __init__(self, parent): assert self._list_type is not None element_name = self._list_type + "_list" SmfNode.__init__(self, parent, element_name) def propagate(self, params): """Generate the value elements. params is a PropDef object where params.value is Tree object whose branches contain the values. """ value_list_node = self.node values = params.value for value_tree in values: node = SmfValueNode(value_list_node, self._list_type) node.propagate(value_tree.value) # Value list classes derrived from ValueList class SmfAstringList(ValueList): _list_type = "astring" class SmfBooleanList(ValueList): _list_type = "boolean" class SmfCountList(ValueList): _list_type = "count" class SmfFmriList(ValueList): _list_type = "fmri" class SmfHostList(ValueList): _list_type = "host" class SmfHostnameList(ValueList): _list_type = "hostname" class SmfIntegerList(ValueList): _list_type = "Integer" class SmfNetAddressList(ValueList): _list_type = "net_address" class SmfNetAddressV4List(ValueList): _list_type = "net_address_v4" class SmfNetAddressV6List(ValueList): _list_type = "net_address_v6" class SmfOpaqueList(ValueList): _list_type = "opaque" class SmfTimeList(ValueList): _list_type = "time" class SmfUriList(ValueList): _list_type = "uri" class SmfUstringList(ValueList): _list_type = "ustring" class SmfPropertyGroup(SmfNode): """Implements property_group element of the DTD""" def __init__(self, parent, pg_type="application"): SmfNode.__init__(self, parent, "property_group") self.pg_type = pg_type def propagate(self, tree): """Create our attributes and our propval element. tree is a Tree containing PG name, property names, property types and values. Attributes are name and type. name comes from the command line and type comes from our pg_type attribute. pg_type defaults to application but can be overridden by a subclass. After creating the attributes, create and propagate the properties. """ pg = self.node pg_name = tree.value pg.setAttribute("name", pg_name) pg.setAttribute("type", self.pg_type) # Create the properties # The branches of tree contain the property names. for prop_tree in tree: prop_name = prop_tree.value # The branches of prop_tree contain property types. for type_tree in prop_tree: prop_type = type_tree.value # The branches of the type tree contain the values. value_tree = type_tree.branches if len(value_tree) == 1: # Only 1 value. Create an propval element. prop_element = SmfPropVal(pg) prop_element.propagate( PropDef(pg_name, prop_name, prop_type, value_tree[0].value)) else: # For multiple values create a property element. prop_element = SmfProperty(pg) prop_element.propagate( PropDef(pg_name, prop_name, prop_type, value_tree)) class SmfDurationPg(SmfPropertyGroup): """Implement the special case duration property group for startd. There are a few things that make this class special. First the PG type is "framework" rather "application". We handle this by setting self.pg_type in the constructor. For manifests the second thing that makes duration interesting is that we do not always need to generate the duration PG in the XML. If the user has specified a duration of contract (daemon is a synonym), there is no need to generate the property group. That is because contract is the default duration. In this case we merely generate a comment. For profiles we always generate a duration if it was specified on the command line. We are assuming that the user wants to override what is in the manifest. """ def __init__(self, parent, params): """Either create a comment or a property group node.""" duration = params.get(NM_DURATION, "transient") if duration == "daemon": duration = "contract" if duration == "wait": duration = "child" if (duration == "contract") and is_manifest(): SmfComment( parent, gettext( "We do not need a duration property group, because " "contract is the default. Search for duration in " "svc.startd(1M). ")) self.duration = "contract" else: SmfPropertyGroup.__init__(self, parent, "framework") self.duration = duration def propagate(self, params): """Propagate the duration PG. If we are generating a manifest and the duration is contract, we don't need to do anything. In this case a commend has already been generated and contract is the default. If the duration is not contract or if we're generating a profile, go ahead and propagate the duration PG. """ if (self.duration == "contract") and is_manifest(): return properties = CLProperties() properties.add_propdef( PropDef("startd", "duration", "astring", self.duration)) SmfPropertyGroup.propagate(self, properties[0]) class ValueType(object): """Provide facilities for manipulating property values of vaious types.""" Implementors = namedtuple( "Implementors", "list_class validator out_translator") # XXX we need an interface to scf_limit(3SCF) and libscf #defines SCF_LIMIT_MAX_FMRI_LENGTH = 628 SCF_LIMIT_MAX_PG_TYPE_LENGTH = 119 SCF_LIMIT_MAX_NAME_LENGTH = 119 SCF_LIMIT_MAX_VALUE_LENGTH = 4095 # Strings for use in constructing FMRIs SCF_FMRI_SVC_PREFIX = "svc:" SCF_FMRI_FILE_PREFIX = "file:" SCF_FMRI_SCOPE_PREFIX = "//" SCF_FMRI_LOCAL_SCOPE = "localhost" SCF_FMRI_SCOPE_SUFFIX = "@localhost" SCF_FMRI_SERVICE_PREFIX = "/" SCF_FMRI_INSTANCE_PREFIX = ":" SCF_FMRI_PROPERTYGRP_PREFIX = "/:properties/" SCF_FMRI_PROPERTY_PREFIX = "/" SCF_FMRI_LEGACY_PREFIX = "lrc:" # Map special XML characters to their entities. Don't need to check # for apostrophe, because we don't use that as a delimiter when writing # attributes. & must be first in the list to avoid converting the # ampersand in other entities. out_map = [ ("&", "&"), ('"', """), ("<", "<"), (">", ">")] def _char_to_entity(value): """Convert XML special characters in value to an entity.""" for c, entity in ValueType.out_map: value = value.replace(c, entity) return value # The following validation functions are modeled on # valid_encoded_value() in scf_type.c. def _validate_astring(value): if len(value) > ValueType.SCF_LIMIT_MAX_VALUE_LENGTH: raise PropValueError(gettext("Value is too long")) def _validate_boolean(value): if value != "true" and value != "false": raise PropValueError( gettext("Value must be true or false")) def _validate_count(value): try: i = long(value) except ValueError: raise PropValueError(gettext("Value is not a number")) if i < 0: raise PropValueError(gettext("Value is negative")) def _validate_fmri(fmri): """Validate the provided FMRI.""" if fmri.startswith(ValueType.SCF_FMRI_FILE_PREFIX): validate_file_fmri(fmri) else: validate_svc_fmri(fmri) def _validate_host(host): """host must be either a hostname or an IP address.""" exceptions = 0 try: check_hostname(host) except PropValueError: exceptions += 1 try: check_ip_addr(host) except: exceptions += 1 if exceptions >= 2: raise PropValueError( gettext("Value is neither a valid host name nor IP address")) def _validate_hostname(host): check_hostname(host) def _validate_integer(value): try: i = long(value) except ValueError: raise PropValueError(gettext("Value is not a number")) def _validate_opaque(str): """Verify the str is opaque. All characters must be hex, there must be an even number, and length must be < 2 * SCF_LIMIT_MAX_VALUE_LENGTH.""" strlen = len(str) if strlen % 2 != 0: raise PropValueError( gettext( "Opaque string must contain an even number of characters")) if strlen >= 2 * ValueType.SCF_LIMIT_MAX_VALUE_LENGTH: raise PropValueError(gettext("Opaque string is too long.")) for c in str: if c not in string.hexdigits: raise PropValueError( gettext("Opaque string must only contain hex digits")) def _validate_v4addr(addr): check_ip_addr_by_type(socket.AF_INET, addr) def _validate_v6addr(addr): check_ip_addr_by_type(socket.AF_INET6, addr) def _validate_ipaddr(addr): check_ip_addr(addr) def _validate_time(time): """time is of the form seconds.nanoseconds.""" if len(time) <= 0: raise PropValueError(gettext("Time value is empty")) comp = time.split(".") if len(comp) >= 1: if len(comp[0]) <= 0: raise PropValueError(gettext("No seconds specified")) try: sec = long(comp[0]) except ValueError: raise PropValueError( gettext("Time contains non-numeric values")) if len(comp) == 2: if len(comp[1]) > 9: raise PropValueError( gettext( "Fractional part of time must contain no more than " "9 digits")) try: nanosec = long(comp[1]) except ValueError: raise PropValueError( gettext("Time contains non-numeric values")) if len(comp) > 2: raise PropValueError( gettext("Time is not of the form seconds.fraction")) def _validate_uri(str): """Verify that str is a valid URI. The following is quoted from RFC 2396: As described in Section 4.3, the generic URI syntax is not sufficient to disambiguate the components of some forms of URI. Since the "greedy algorithm" described in that section is identical to the disambiguation method used by POSIX regular expressions, it is natural and commonplace to use a regular expression for parsing the potential four components and fragment identifier of a URI reference. The following line is the regular expression for breaking-down a URI reference into its components. ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? 12 3 4 5 6 7 8 9 The numbers in the second line above are only to assist readability; they indicate the reference points for each subexpression (i.e., each paired parenthesis). We refer to the value matched for subexpression as $. For example, matching the above expression to http://www.ics.uci.edu/pub/ietf/uri/#Related results in the following subexpression matches: $1 = http: $2 = http $3 = //www.ics.uci.edu $4 = www.ics.uci.edu $5 = /pub/ietf/uri/ $6 = $7 = $8 = #Related $9 = Related where indicates that the component is not present, as is the case for the query component in the above example. Therefore, we can determine the value of the four components and fragment as scheme = $2 authority = $4 path = $5 query = $7 fragment = $9 We will consider the URI to be valid if the length of the path component is greater than 0 and if the length of the entire URI is less than SCF_LIMIT_MAX_VALUE_LENGTH. """ if len(str) > ValueType.SCF_LIMIT_MAX_VALUE_LENGTH: raise PropValueError(gettext("URI is too long")) if len(str) == 0: raise PropValueError(gettext("URI has zero length")) mo = NVPair.comp_uri_re.match(str) if mo is None: raise PropValueError(gettext("URI is invalid")) if mo.group() != str: # Didn't match the whole string. raise PropValueError(gettext("URI is invalid")) if len(mo.group(5)) == 0: raise PropValueError( gettext("URI does not contain a path component")) def _validate_ustring(str): """Validate UTF-8 strings.""" check_utf8(str) _value_type_map = { "astring": Implementors( SmfAstringList, _validate_astring, _char_to_entity), "boolean": Implementors(SmfBooleanList, _validate_boolean, None), "count": Implementors(SmfCountList, _validate_count, None), "fmri": Implementors(SmfFmriList, _validate_fmri, None), "host": Implementors(SmfHostList, _validate_host, None), "hostname": Implementors( SmfHostnameList, _validate_hostname, None), "integer": Implementors(SmfIntegerList, _validate_integer, None), "net_address": Implementors( SmfNetAddressList, _validate_ipaddr, None), "net_address_v4": Implementors( SmfNetAddressV4List, _validate_v4addr, None), "net_address_v6": Implementors( SmfNetAddressV6List, _validate_v6addr, None), "opaque": Implementors( SmfOpaqueList, _validate_opaque, None), "time": Implementors(SmfTimeList, _validate_time, None), "uri": Implementors( SmfUriList, _validate_uri, _char_to_entity), "ustring": Implementors( SmfUstringList, _validate_ustring, _char_to_entity) } def valid_type(cls, prop_type): """Return True if prop_type is a valid property type.""" return prop_type in cls._value_type_map valid_type = classmethod(valid_type) def list_class(cls, prop_type): """Return the class that implements value lists for prop_type.""" implementors = cls._value_type_map.get(prop_type) if implementors is None: return None return implementors.list_class list_class = classmethod(list_class) def translate_output(cls, prop_type, value): """Translate internal representation for output.""" implementors = cls._value_type_map.get(prop_type) if implementors is None: return value trans = implementors.out_translator if trans is None: return value return trans(value) translate_output = classmethod(translate_output) def valid_value(cls, prop_type, value): """Insure that value is valid for prop_type.""" implementor = cls._value_type_map.get(prop_type) if implementor is None: raise CLError( gettext( "\"{0}\" is not a valid property type.").format(prop_type)) func = implementor.validator if func is None: return True return func(value) valid_value = classmethod(valid_value) class SmfProperty(SmfNode): """Implements the property DTD element.""" def __init__(self, parent): SmfNode.__init__(self, parent, "property") def propagate(self, params): """Set attributes of element and generate enclosed value list. params is a PropDef object. """ node = self.node node.setAttribute("name", params.prop_name) prop_type = params.prop_type if prop_type == CLProperties.UNSPECIFIED_PROP_TYPE: prop_type = CLProperties.DEFAULT_PROP_TYPE node.setAttribute("type", prop_type) cls = ValueType.list_class(prop_type) if cls is None: raise CLError( gettext("\"{0}\" is an unsupported property type.").format( params.prop_type)) value_list = cls(node) value_list.propagate(params) class SmfPropVal(SmfNode): """Implements the propval DTD element.""" def __init__(self, parent): SmfNode.__init__(self, parent, "propval") def propagate(self, params): """Set the attributes of the propval node. params is a PropDef. """ prop = self.node prop.setAttribute("name", params.prop_name) prop_type = params.prop_type if prop_type == CLProperties.UNSPECIFIED_PROP_TYPE: if is_manifest(): # For manifests use the default prop type. # Profiles don't require a prop type prop_type = CLProperties.DEFAULT_PROP_TYPE prop.setAttribute("type", prop_type) else: prop.setAttribute("type", prop_type) self.property_type = prop_type prop.setAttribute("value", params.value) def translate_attribute(self, name, value): if name == "value": return ValueType.translate_output(self.property_type, value) else: return value # Classes for generating the templates section: class SmfCommonName(SmfNode): """Generate the common name element.""" def __init__(self, parent): SmfNode.__init__(self, parent, "common_name") def propagate(self, params): """Generate a common_name element with comment to explain.""" cn = self.node loctext = SmfLoctext(cn) loctext.propagate( gettext("Replace this comment with a short name for the service.")) class SmfDescription(SmfNode): """Generate the description element""" def __init__(self, parent): SmfNode.__init__(self, parent, "description") def propagate(self, params): """Generate a description element with comment to explain.""" desc = self.node loctext = SmfLoctext(desc) loctext.propagate( gettext( "Replace this comment with a brief description of the " "service")) class SmfTemplate(SmfNode): def __init__(self, parent): SmfNode.__init__(self, parent, "template") def propagate(self, params): """Generate a simple template.""" template = self.node cn = SmfCommonName(template) cn.propagate(params) desc = SmfDescription(template) desc.propagate(params) class SmfService(SmfNode): """Implements the service element of the DTD.""" # If we're generating a profile, we don't always need to generate an # instance specification. We only need to do it if one of the # following appears on the command line. instance_generators = [NM_ENABLED, NM_INST_NAME, NM_INST_PROP] # Map to tell us the dependency (first item in the list) and dependent # (second item in the list) to generate for rc-script manifests. Index # into the map is the run level. run_level_map = { "s": [SmfSingleUserDependency, SmfMultiUserDependent], "S": [SmfSingleUserDependency, SmfMultiUserDependent], "0": [None, None], "1": [SmfSingleUserDependency, SmfMultiUserDependent], "2": [SmfMultiUserDependency, SmfMultiUserServerDependent], "3": [SmfMultiUserServerDependency, None], "4": [SmfMultiUserServerDependency, None], "5": [None, None], "6": [None, None] } def __init__(self, parent): SmfNode.__init__(self, parent, "service") def propagate(self, params): """Add service attributes and child elements to the DOM. First set our name, version and type attributes. Name comes from the command line parameters at params. Version and type are always set to 1 and service Additional subelements that are added to the DOM and propagated are: dependency dependents start, stop and refresh methods duration property group Next add any property groups specified on the command line to the DOM. Propagate the property groups. Finally add an instance to the DOM and propagate it. """ manifest = is_manifest() svc = self.node name = params.get(NM_SVC_NAME, None) if name is None: raise CLError(gettext("No {0} specified.".format(NM_SVC_NAME))) svc.setAttribute("name", name) svc.setAttribute("version", "1") svc.setAttribute("type", "service") # Create the subelements # # For manifests we automatically generate a number of elements in # order to have a working manifest. For profiles, on the other # hand, we only want to generate elements that were specified on # the command line. if manifest: is_periodic = False # The list of classes to propagate varies if we are doing an rc # script. rc_params = params.get(NM_RC_SCRIPT) if rc_params is None: # Not an rc script # If we have a "period" pair, then we # want to emit a periodic_method element # as opposed to an exec_method for the # start method. # # Additionally, periodic services don't # get stop or refresh methods if (params.get(NM_PERIOD) is None and params.get(NM_INTERVAL) is None): propagate = [ SmfAutoDependency(svc), SmfExecMethodStart(svc), SmfExecMethodStop(svc), SmfExecMethodRefresh(svc)] elif params.get(NM_PERIOD) is not None: is_periodic = True propagate = [ SmfAutoDependency(svc), SmfPeriodicMethod(svc)] else: is_periodic = True propagate = [ SmfAutoDependency(svc), SmfScheduledMethod(svc)] else: propagate = [] # We're processing an rc script. Figure out if we need to # generate a milestone dependency and dependent. run_level = rc_params.run_level milestones = self.run_level_map.get(run_level) if milestones is None: raise CLError( gettext( "\"{0}\" is an invalid run level for {1}").format( run_level, NM_RC_SCRIPT)) for milestone in milestones: if milestone is not None: propagate.append(milestone(svc)) propagate.append(SmfExecMethodRcStart(svc)) propagate.append(SmfExecMethodRcStop(svc)) propagate.append(SmfExecMethodRefresh(svc)) if is_periodic is False: propagate.append(SmfDurationPg(svc, params)) else: propagate = [] if NM_START in params: propagate.append(SmfExecMethodStart(svc)) if NM_STOP in params: propagate.append(SmfExecMethodStop(svc)) if NM_REFRESH in params: propagate.append(SmfExecMethodRefresh(svc)) # Propagate the elements for element in propagate: element.propagate(params) # Create any property groups pg_propagate = extract_pg_info(svc, params, NM_SVC_PROP) for pg, pg_def in pg_propagate: pg.propagate(pg_def) # Create and propagate an instance if necessary. If we're # producing a manifest, we always generate the instance. For # profiles, we only generate the instance if we have something to # put in it. if not manifest: for name in self.instance_generators: if name in params: break else: # No instance generators. return inst = SmfInstance(svc) inst.propagate(params) # If we're doing a manifest, generate a simple template. if manifest: template = SmfTemplate(svc) template.propagate(params) class SmfServiceFmri(SmfNode): """Implements the service_fmri element of the DTD.""" def __init__(self, parent, fmri): SmfNode.__init__(self, parent, "service_fmri") self.fmri = fmri def propagate(self, params): self.node.setAttribute("value", self.fmri) # Classes for processing command line name value pairs class NVPair(object): """Base class for processing command line -s name=value pairs: The purpose of the NVPair class is to process the name/value pairs specified with the -s option on the command line and add them to the params dictionary. The SmfNode classes then obtain command line information from the params dictionary. The work is done by the constructor with the help of the update_params() method. In most cases the update_params() method provided by the base class will suffice, but it can be overriden as is done by the Property class. update_params() is driven by 5 class attributes -- _count, _max, _name, _key and _legal_values. The objects add the value to a dictionary using the name as the key. This work is done in the constructor with the help of update_params() method. Inheriting classes are expected to override update_params() to handle special cases. The default implementation of update_values() calls validate() to check the provided value. This class also provides a default implementation of validate() which merely checks to see if value is in the list of _legal_values. Inheriting classes should feel free to override this method. A class method, describe_yourself(), is used by the help subcommand. It writes a description of the name to stdout using the safe_so() function. It is driven by the _description and _value_mnemonic attributes. All subclasses should override _description and _value_mnemonic. Most subclasses will be able to get this base class to do their work by merely overriding these class attributes. Inheriting classes must create their own definitions of _count, _description, _name, _key and _value_mnemonic. They should also override _legal_values if they plan to use the default validate() function. They can also override _max if appropriate. The usage of these variables is as follows: _count Keeps track of the number of instances of the class. for comparison to _max. (read/write) _description A string describing the -s name that is implemented by the class. _legal_values A list of legal values for use with the default validate() function. _max Maximum number of instances that are allowed for the class. -1 implies no maximum. (read-only) _name name part of -s name=value that this class represents. (read-only) _key Key for referencing the params dictionary. Except in the case of synonyms it should be the same as _name. (read-only). _value_mnemonic - Textual representation of the value part of the name/value pair for use in printing a representation of the command in help messages. We also have several class variables to assist in regular expression matching of names, e.g. service names or property names. They are: domain_re - Domain component of a host name. comp_domain_re - RE compiled version of domain_re ident_re - A regular expression for an identifier. comp_ident_re - The RE compiled version of ident_re. name_re - A regular expression for property group, property and instance names. comp_name_re - The RE compiled version of name_re. service_re - A regular expression for service names. comp_service_re - The RE compiled version of service_re. uri_re - A regular expression for URIs comp_uri_re - Compiled version of uri_re. """ _count = 0 _description = None _legal_values = None _max = 1 _name = "undefined" _key = "" _value_mnemonic = None domain_re = r"(?!-)[A-Z\d-]{1,63}(?= 0) and (count > max_inst): raise CLError( gettext( "No more than {0} occurrence of \"{1}\" is " "allowed.").format(max_inst, self._name)) self.update_params(params, self._key, value) self.__class__._count = count def update_params(self, params, key, value): """Validate value and add it to params at key. If key already exists in params, throw an exception. """ if key in params: raise CLError( gettext( "Multiple occurrences of \"{0}\" are prohibited.").format( self._name)) # Save value for later validation. self.value = value params[key] = value def validate(self, params): """Check to see if value is in list of legal values.""" if self._legal_values is None: return if self.value in self._legal_values: return raise CLError( gettext("\"{0}\" is not a legal value for {1}").format( self.value, self._name)) def describe_yourself(cls, indent="", addindent=" "): """Write a description of name to standard out. Parameters: indent - Indentation string for the first line of output. addindent - String to be added to indent for subsequent lines. """ assert cls._description is not None assert cls._value_mnemonic is not None safe_so( textwrap.fill( "-s {0}={1}".format(cls._name, cls._value_mnemonic), 75, initial_indent=indent, subsequent_indent=indent + addindent)) indent = indent + addindent safe_so(textwrap.fill( cls._description, 75, initial_indent=indent, subsequent_indent=indent)) describe_yourself = classmethod(describe_yourself) class BundleType(NVPair): _count = 0 _legal_values = ["manifest", "profile"] _name = NM_BUNDLE_TYPE _key = NM_BUNDLE_TYPE _value_mnemonic = "type" _description = gettext( 'Specifies the type of bundle to generate. Legal values are ' '"manifest" and "profile". The default is manifest.') def update_params(self, params, key, value): if value != "manifest": SmfNode.is_manifest = False NVPair.update_params(self, params, key, value) class Duration(NVPair): _count = 0 _legal_values = ["contract", "child", "transient", "daemon", "wait"] _key = NM_DURATION _name = NM_DURATION _value_mnemonic = "service_model" _description = gettext("{0} is a synonym for {1}.").format( NM_DURATION, NM_MODEL) class Enabled(NVPair): _count = 0 _legal_values = ["true", "false"] _name = NM_ENABLED _key = NM_ENABLED _value_mnemonic = "boolean" _description = gettext( 'Specifies whether or not the service should be enabled. Legal ' 'values are "true" and "false".') class InstanceName(NVPair): _count = 0 _name = NM_INST_NAME _key = NM_INST_NAME _value_mnemonic = "name" _description = gettext("Specifies the name of the instance.") def validate(self, params): if check_name(self.comp_name_re, self.value): return raise CLError(gettext("\"{0}\" is an invalid {1}").format( self.value, self._name)) class Model(Duration): """model is a synonym for duration""" _legal_values = ["contract", "child", "transient", "daemon", "wait"] _name = NM_MODEL _value_mnemonic = "service_model" _description = gettext( 'Sets the service model. This is the value of the ' 'startd/duration property. Refer to svc.startd(1M). ' 'Model can be be set to one of "contract", "child" or ' '"transient". ' 'Also "daemon" can be use as a synonym for "contract", and ' '"wait" can be used as a synonym for "child". The default value ' 'is "transient".') class Period(NVPair): _count = 0 _name = NM_PERIOD _key = NM_PERIOD _value_mnemonic = "period" _description = gettext( "") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( NM_START, self._name))) try: validate_time(self.value) except PropValueError as e: raise CLError( gettext( "\"{0}\" is not a valid time: {1}".format( self._name, str(e)))) class RcScript(NVPair): _count = 0 _name = NM_RC_SCRIPT _key = NM_RC_SCRIPT _value_mnemonic = "script_path:run_level" _description = gettext( "Generates a manifest that facilitates conversion of a legacy rc " "script to an SMF service. script_path is the path to the rc " "script and run_level is the run level of the script (see " "init(1M)). script_path is used to generate the start and stop " "method elements in the manifest. run_level is used to generate " "dependencies so that the script runs at the appropriate time.") def update_params(self, params, key, value): rc_def = value.rpartition(":") if (len(rc_def[0]) == 0) or (rc_def[1] != ":") or \ (len(rc_def[2]) == 0): raise CLError( gettext( "Value for {0} must contain 2 colon " "separated fields").format( NM_RC_SCRIPT)) NVPair.update_params(self, params, key, RcDef(rc_def[0], rc_def[2])) def validate(self, params): """Insure that rc-script not specified with exec methods. We could also check run levels at this point, but it is more convenient to do it in SmfService where we already have dictionary of run levels. """ if NM_START in params: raise CLError( gettext( "\"{0}\" should not be specfied with \"{1}\"").format( self._name, NM_START)) if NM_STOP in params: raise CLError( gettext( "\"{0}\" should not be specfied with \"{1}\"").format( self._name, NM_STOP)) class RefreshMethod(NVPair): _count = 0 _name = NM_REFRESH _key = NM_REFRESH _value_mnemonic = "command" _description = gettext( "Specifies the command to execute when the service is " "refreshed. White space is allowed in the value. The method " "tokens that are introduced by % as documented in smf_method(5) " "are allowed. :true is the default.").format(NM_REFRESH) class StartMethod(NVPair): _count = 0 _name = NM_START _key = NM_START _value_mnemonic = "command" _description = gettext( "Specifies the command to execute when the service is started. " "White space is allowed in the value. The method tokens that " "are introduced by % as documented in smf_method(5) are allowed. " ":true is allowed. {0} is required for manifests.").format( NM_START) def validate(self, params): """Don't specify with rc-script.""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specfied with \"{1}\"").format( self._name, NM_RC_SCRIPT)) class StopMethod(NVPair): _count = 0 _name = NM_STOP _key = NM_STOP _value_mnemonic = "command" _description = gettext( "Specifies the command to execute when the service is stopped. " "White space is allowed in the value. The method tokens that " "are introduced by % as documented in smf_method(5) are allowed. " "Both :true and :kill are allowed. :true is the default for " "transient services and :kill is the default for contract and " "child services.") def validate(self, params): """Don't specify with rc-script.""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specfied with \"{1}\"").format( self._name, NM_RC_SCRIPT)) class ServiceName(NVPair): _count = 0 _name = NM_SVC_NAME _key = NM_SVC_NAME _value_mnemonic = "name" _description = ( gettext( "Specifies the name of the service. {0} is required.").format( NM_SVC_NAME)) def validate(self, params): """Verify that our value is a valid service name.""" if check_name(self.comp_service_re, self.value): return raise CLError(gettext("\"{0}\" is not a valid {1}").format( self.value, self._name)) class Property(NVPair): """Base class for instance-property and service-property: This class is special in two ways. Because multiple occurrences of these names are allowed on the command line, we cannot store a simple value in the params dictionary. Instead a CLProperties object is stored in the params dictionary. The second difference is that the values associated with these names are complex. The value actually contains property group name, property name, property type and value all separated by colons. We break these apart and add them as a PropDef to the CLProperties object. All this is handled by the update_params() method that we provide. """ _value_mnemonic = "pg_name:prop_name:prop_type:value" _description = gettext( "This option is used to " "create a property group named pg_name. The PG will have a " "single property named prop_name with a single value that is of " "type prop_type. Zero or more declarations may be specified.") def update_params(self, params, key, value): # Need to get the 4 parts of the property definition prop_def_components = value.split(":", 3) if len(prop_def_components) != 4: raise CLError( gettext( "Property definition \"{0}\" must have 4 " "components.").format( value)) if prop_def_components[2] == CLProperties.UNSPECIFIED_PROP_TYPE: raise CLError( gettext( "\"{0}\" is an invalid property type in {1}.").format( prop_dev_components[2], value)) if len(prop_def_components[2]) == 0: prop_def_components[2] = CLProperties.UNSPECIFIED_PROP_TYPE prop_def = PropDef(*prop_def_components) if key in params: prop_tree = params[key] else: prop_tree = CLProperties() params[key] = prop_tree prop_tree.add_propdef(prop_def) self.value = value self.prop_def = prop_def def validate(self, params): """Validate the elements of our prop_def.""" prop_def = self.prop_def # Validate PG and property names pg_name = prop_def.pg_name prop_name = prop_def.prop_name if len(pg_name) == 0: raise CLError( gettext( "Property group name is empty in \"{0}\".").format( self.value)) if len(prop_name) == 0: raise CLError( gettext( "Property name is empty in \"{0}\"").format( self.value)) if not check_name(self.comp_name_re, pg_name): raise CLError( gettext( "\"{0}\" is an illegal property group name " "in \"{1}\".").format( pg_name, self.value)) if not check_name(self.comp_name_re, prop_name): raise CLError( gettext( "\"{0}\" is an illegal property name in \"{1}\".").format( prop_name, self.value)) prop_type = prop_def.prop_type # User is not required to specify the property type. If we're # producing a manifest, we'll use the default property type. If # we're generating a profile, we don't have the information that we # need for validation of property type or value. if prop_type == CLProperties.UNSPECIFIED_PROP_TYPE: if is_manifest(): prop_type = CLProperties.DEFAULT_PROP_TYPE else: return if not ValueType.valid_type(prop_type): raise CLError( gettext( "\"{0}\" is an invalid property type in \"{1}\".").format( prop_type, self.value)) try: ValueType.valid_value(prop_type, prop_def.value) except PropValueError as err: raise CLError("{0}: \"{1}\".".format( err.args[0], self.value)) class InstanceProperty(Property): _count = 0 _max = -1 # No maximum _name = NM_INST_PROP _key = NM_INST_PROP class ServiceProperty(Property): _count = 0 _max = -1 # No maximum _name = NM_SVC_PROP _key = NM_SVC_PROP class Day(NVPair): _count = 0 _name = NM_DAY _key = NM_DAY _value_mnemonic = "day" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) # First make sure the value is sane val = self.value.title() try: idx = int(val) if idx < -7 or idx > 7 or idx == 0: raise CLError( gettext( "\"{0}\" must be between 1 and 7 or -7 and -1".format( self._name))) except ValueError: # Not an integer, so it might be a name if val not in calendar.day_name and val not in calendar.day_abbr: raise CLError( gettext( "\"{0}\" must be a valid weekday".format( self._name))) class DayOfMonth(NVPair): _count = 0 _name = NM_DAY_OF_MONTH _key = NM_DAY_OF_MONTH _value_mnemonic = "day_of_month" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) try: idx = int(self.value) if idx > 31 or idx < -31 or idx == 0: raise CLError( gettext( "\"{0}\" must be between 1 and 31 or -1 and -31". format(self.value))) except ValueError: raise CLError( gettext( "\"{0}\" must be between 1 and 31 or -1 and -31".format( self.value))) class Frequency(NVPair): _count = 0 _name = NM_FREQUENCY _key = NM_FREQUENCY _value_mnemonic = "frequency" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) try: val = int(self.value) if val < 1: raise CLError( gettext( "\"{0}\" must be a positive integer".format( self._name))) except ValueError: raise CLError( gettext( "\"{0}\" must be a positive integer".format( self._name))) # and finally, make sure a reference point exists graph = {NM_YEAR: [], NM_MONTH: [NM_YEAR], NM_WEEK_OF_YEAR: [NM_YEAR], NM_WEEKDAY_OF_MONTH: [NM_MONTH], NM_DAY: [NM_WEEK_OF_YEAR, NM_WEEKDAY_OF_MONTH], NM_DAY_OF_MONTH: [NM_MONTH], NM_HOUR: [NM_DAY, NM_DAY_OF_MONTH], NM_MINUTE: [NM_HOUR]} entries = {"year": [NM_YEAR], "month": [NM_MONTH], "week": [NM_WEEK_OF_YEAR, NM_DAY, NM_DAY_OF_MONTH], "day": [NM_DAY, NM_DAY_OF_MONTH], "hour": [NM_HOUR], "minute": [NM_MINUTE]} nodes = None try: nodes = entries[params[NM_INTERVAL]] except: # If the interval does not exist or is # invalid, we'll catch that here. Since # other parts of the code enforce the # requirement for a valid interval, we # can ignore it. Without a valid interval, # we can't tell if a valid reference is # defined, anyhow. return while True: found_node = None for node in nodes: if node in params: found_node = node break if found_node is None: raise CLError( gettext( "For frequencies other than 1, a full reference" + " point must be defined")) nodes = graph[node] # We've reached NM_YEAR, the top of the graph if len(nodes) == 0: break class Hour(NVPair): _count = 0 _name = NM_HOUR _key = NM_HOUR _value_mnemonic = "hour" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) try: val = int(self.value) if val > 23 or val < -24: raise CLError( gettext( "\"{0}\" must be an integer between -24 and 23".format( self._name, NM_HOUR))) except ValueError: raise CLError( gettext( "\"{0}\" must be an integer between -24 and 23".format( self._name, NM_HOUR))) class Interval(NVPair): _count = 0 _name = NM_INTERVAL _key = NM_INTERVAL _value_mnemonic = "interval" _description = gettext("") _valid_intervals = ("year", "month", "week", "day", "hour", "minute") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) if self.value not in self._valid_intervals: raise CLError( gettext( "\"{0}\" must be one of: {1}".format( self._name, ", ".join(self._valid_intervals)))) # If we've gotten this far, we must be valid # We can't use a graph like we did with missing # reference points in the Frequency validator # because we are checking for continuity and not # existence. Using the graph approach, one undefined # constraint means you don't know which edges to # traverse. entries = {"year": [[NM_MONTH, NM_WEEKDAY_OF_MONTH, NM_DAY, NM_HOUR, NM_MINUTE], [NM_MONTH, NM_DAY_OF_MONTH, NM_DAY, NM_HOUR, NM_MINUTE], [NM_WEEK_OF_YEAR, NM_DAY, NM_HOUR, NM_MINUTE]], "month": [[NM_WEEKDAY_OF_MONTH, NM_DAY, NM_HOUR, NM_MINUTE], [NM_DAY_OF_MONTH, NM_HOUR, NM_MINUTE]], "week": [[NM_DAY, NM_HOUR, NM_MINUTE], [NM_MONTH, NM_DAY_OF_MONTH, NM_HOUR, NM_MINUTE], [NM_MONTH, NM_WEEKDAY_OF_MONTH, NM_DAY, NM_HOUR, NM_MINUTE]], "day": [[NM_HOUR, NM_MINUTE]], "hour": [[NM_MINUTE]], "minute": [[]]} paths = entries[self.value] last_node = None for path in paths: contiguous = True loop_last = None for node in path: if node not in params: contiguous = False elif not contiguous: loop_last = node break # if we found a contiguous path, we're done! if contiguous: break if last_node is not None: raise CLError( gettext( "Schedule is missing at least one constraint" + " above \"{0}\"".format(last_node))) class Minute(NVPair): _count = 0 _name = NM_MINUTE _key = NM_MINUTE _value_mnemonic = "minute" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) try: val = int(self.value) if val > 59 or val < -60: raise CLError( gettext( "\"{0}\" must be an integer between -60 and 59".format( self._name, NM_MINUTE))) except ValueError: raise CLError( gettext( "\"{0}\" must be an integer between -60 and 59".format( self._name, NM_MINUTE))) class Month(NVPair): _count = 0 _name = NM_MONTH _key = NM_MONTH _value_mnemonic = "month" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) val = self.value.title() try: idx = int(val) if idx > 12 or idx < -12 or idx == 0: raise CLError( gettext( "\"{0}\" must be between -12 and -1, 1 and 12, or" " be a valid name or abbreviation of a month".format( self.value))) except: if val not in calendar.month_name and \ val not in calendar.month_abbr: raise CLError( gettext( "\"{0}\" is not the name of a month".format( self.value))) class Timezone(NVPair): _count = 0 _name = NM_TIMEZONE _key = NM_TIMEZONE _value_mnemonic = "timezone" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) class WeekOfYear(NVPair): _count = 0 _name = NM_WEEK_OF_YEAR _key = NM_WEEK_OF_YEAR _value_mnemonic = "week_of_year" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) try: idx = int(self.value) if idx > 53 or idx < -53 or idx == 0: raise CLError( gettext( "\"{0}\" must be between 1 and 53 or -1 and -53". format(self._name))) except ValueError: raise CLError( gettext( "\"{0}\" must be between 1 and 53 or -1 and -53".format( self._name))) class WeekdayOfMonth(NVPair): _count = 0 _name = NM_WEEKDAY_OF_MONTH _key = NM_WEEKDAY_OF_MONTH _value_mnemonic = "weekday_of_month" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) try: idx = int(self.value) if idx < -5 or idx > 5 or idx == 0: raise CLError( gettext( "\"{0}\" must be between 1 and 5 or -1 and -5".format( self._name))) except ValueError: raise CLError( gettext( "\"{0}\" must be between 1 and 5 or -1 and -5".format( self._name))) class Year(NVPair): _count = 0 _name = NM_YEAR _key = NM_YEAR _value_mnemonic = "year" _description = gettext("") def validate(self, params): """Don't specify with rc-script or without start-method""" if NM_RC_SCRIPT in params: raise CLError( gettext( "\"{0}\" should not be specified with \"{1}\"".format( self._name, NM_RC_SCRIPT))) if NM_START not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_START))) if NM_INTERVAL not in params: raise CLError( gettext( "\"{0}\" must be specified with \"{1}\"".format( self._name, NM_INTERVAL))) try: int(self.value) except ValueError: raise CLError( gettext( "\"{0}\" must be an integer".format(self._name))) def monitor_state(params, parser): """Wait for service to move past offline state.""" # We need to wait for the service to transition out of it's offline # state. Research using svcprop -w shows that there are some nasty # race conditions. First we can start svcprop -w before the manifest # import completes. Second, we can start svcprop -w after the final # state is achieved, and svcprop -w will never exit. We'll resort to # the more straight forward polling. enabled = params.get(NM_ENABLED, "true") if enabled == "true": desired_state = "online" else: desired_state = "disabled" service_name = params.get(NM_SVC_NAME) instance_name = params.get(NM_INST_NAME, "default") fmri = "svc:/{0}:{1}".format(service_name, instance_name) safe_so( gettext( "Waiting for {0} to reach {1} state.\n" "It is safe to interrupt.").format( service_name, desired_state)) timeout = int(SmfNode.method_timeout) elapsed = 0 duration = 1 cmd = ["/usr/bin/svcprop", "-p", "restarter/state", fmri] while elapsed < timeout: try: svcprop = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = svcprop.communicate() except OSError as e: parser.error( gettext( "Unable to run svcprop. {0}.").format( os.strerror(e.errno))) if svcprop.returncode != 0: if ("doesn't match any entities" not in result[1] and "Couldn't find property group" not in results[1]): parser.error( gettext( "Problem in running svcprop. {0}").format( result[1])) else: if (result[0] != "") and (not result[0].startswith("offline")): return time.sleep(duration) elapsed += duration if duration < 4: duration += 1 state = result[0].rstrip(string.whitespace) safe_se( gettext( "{0}: Warning: service, {1}, is {2} after {3} seconds.\n").format( SmfNode.pname, fmri, state, elapsed)) def manifest_import(params, parser): """Restart manifest-import to import/apply the bundle.""" cmd = ["/usr/sbin/svcadm", "restart", "manifest-import"] try: svcadm = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = svcadm.communicate() except OSError as e: parser.error( gettext( "Unable to run svcadm to restart manifest-import. " "{0}.").format(os.strerror(e.errno))) if svcadm.returncode != 0: safe_se( gettext( "{0}: svcadm is unable to restart manifest-import\n").format( SmfNode.pname)) safe_se(result[1]) sys.exit(1) if not is_manifest(): # Can't monitor for state transitions with profiles, because there # is no useful way for us to determine what to monitor. return monitor_state(params, parser) def process_subcommands(bundle, args, name_map): """Process the subcommands. Parameters: bundle - main object for our bundle args - list of command line arguments following the options name_map - a dictionary mapping command line names to their implementing classes. Currently, the only subcommand that we support is the help command. If help is used alone, a brief summary of the command is presented. args beyond help are names used in the -s option. If names are specified, the help message focuses on them and explains the legal values. """ parser = bundle.parser if len(args) == 0: return if args[0] != "help": raise CLError( gettext("{0} is an invalid subcommand.").format(args[0])) if len(args) == 1: # Just a simple help message. name_list = list(name_map.keys()) name_list.sort() names = ", ".join(name_list) safe_so(parser.get_usage()) safe_so( gettext( "-i is used to install the generated manifest or bundle.")) safe_so( gettext( "-o specifies where to write the generated manifest or " "bundle.\n")) msg = gettext("Legal names for -s are: ") + names + "." safe_so(textwrap.fill(msg, 75)) safe_so( gettext( "\nFor more information on a name use \"{0} " "help name\".").format(bundle.pname)) else: for name in args[1:]: cls = name_map.get(name) if cls is None: raise CLError( gettext( "{0} is an invalid name for -s").format(name)) cls.describe_yourself() sys.exit(0) def process_command_line(bundle, params): """Process command line parameters and add them to params dictionary. The created parser will be saved in SmfNode.parser, so that it can be used to generate error messages. """ pairs_seen = [] # Map the command line names to their implementing classes. name_map = { NM_BUNDLE_TYPE: BundleType, NM_DAY: Day, NM_DAY_OF_MONTH: DayOfMonth, NM_DURATION: Duration, NM_ENABLED: Enabled, NM_FREQUENCY: Frequency, NM_HOUR: Hour, NM_INST_NAME: InstanceName, NM_INST_PROP: InstanceProperty, NM_INTERVAL: Interval, NM_MINUTE: Minute, NM_MODEL: Model, NM_MONTH: Month, NM_PERIOD: Period, NM_RC_SCRIPT: RcScript, NM_REFRESH: RefreshMethod, NM_START: StartMethod, NM_STOP: StopMethod, NM_SVC_NAME: ServiceName, NM_SVC_PROP: ServiceProperty, NM_TIMEZONE: Timezone, NM_WEEK_OF_YEAR: WeekOfYear, NM_WEEKDAY_OF_MONTH: WeekdayOfMonth, NM_YEAR: Year } parser = OptionParser( usage="%prog [-i | -o output_file] -s name=value ... [help [name]]", prog=bundle.pname) SmfNode.parser = parser if len(sys.argv) <= 1: # No arguments on the command line. Just generate the help command # and exit. process_subcommands(bundle, ["help"], name_map) sys.exit() parser.disable_interspersed_args() parser.add_option("-s", type="string", dest="nvpairs", action="append") parser.add_option( "-o", type="string", dest="output_file", action="store", default=None) parser.add_option( "-i", action="store_true", dest="install", default=None) (options, args) = parser.parse_args() optlist = options.nvpairs if len(args) > 0: try: process_subcommands(bundle, args, name_map) except CLError as e: parser.error(e.args[0]) if optlist is None: # No NVPairs were spepecified. parser.error(gettext("Use -s to specify bundle parameters.")) output = options.output_file # Make sure that file name ends in .xml if output is not None: i = output.rfind(".xml") if (len(output) - i) != len(".xml"): parser.error( gettext( 'Output file name must end in ".xml" for SMF to ' 'process it.')) params[NM_OUTPUT_FILE] = output # See if -i was specified. It is illegal to specify both -i and -o # together. install = options.install if (install is not None) and (output is not None): parser.error( gettext("-i and -o are mutually exclusive")) if install is not None: SmfNode.installing = True # From this point on, we don't want to print the usage message for # errors. We just want to have use the standard error message. parser.set_usage(SUPPRESS_USAGE) for opt in optlist: nvpair = opt.partition("=") if nvpair[1] != "=": parser.error( gettext( "\"{0}\" contains no equal sign. Parameters " "should have the form of name=value.").format(opt)) value = nvpair[2] if value == "": parser.error( gettext("\"{0}\" contains no value. Parameters " "should have the form of name=value.").format(opt)) name = nvpair[0] cls = name_map.get(name) if cls is None: parser.error(gettext("\"{0}\" is an invalid name.").format(name)) try: inst = cls(value, params) except CLError as err: parser.error(err.args[0]) pairs_seen.append(inst) # Now that we've processed all of the NVPairs, we can determine the # output file for use with -i. The output file is derived from the # service name if install is not None: service_name = params.get(NM_SVC_NAME) if service_name is None: parser.error( gettext( "Specify a service name using -s {0}.").format( NM_SVC_NAME)) file = os.path.basename(service_name) if is_manifest(): file = "/lib/svc/manifest/site/" + file + ".xml" else: file = "/etc/svc/profile/site/" + file + ".xml" params[NM_OUTPUT_FILE] = file # Go through the list of options seen and verify them as needed for obj in pairs_seen: try: obj.validate(params) except CLError as err: parser.error(err.args[0]) def start_bundle(): """Initialize the DOM.""" SmfNode.implementation = getDOMImplementation() doc_type = SmfDocumentType() doc = SmfDocument("service_bundle", doc_type) doc = doc.node SmfNode.document = doc top = doc.documentElement bundle = SmfBundle(doc, top) pname = os.path.basename(sys.argv[0]) SmfNode.pname = pname comment = SmfComment( doc, gettext("Manifest created by ") + pname + time.strftime( " (%Y-%b-%d %H:%M:%S" "%z)"), top) return bundle def validate_and_write(params, parser, channel, filename): """Validate the temp file and write to specified location. This function is called when the manifest has been written to a temporary file. channel is the open temporary file, and filename is the name of the temporary file. We use params to determine where the final manifest should be written, and parser for printing error messages. """ # Make sure that data has been written out. channel.flush() # Use svccfg to validate the manifest cmd = ["/usr/sbin/svccfg", "validate"] cmd.append(filename) try: svccfg = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = svccfg.communicate() except OSError as e: parser.error( gettext( "Problem while using svccfg to validate the manifest. " "{0}.").format(os.strerror(e.errno))) if svccfg.returncode != 0: safe_se( gettext( "{0}: svccfg validate found the following errors in the " "manifest which is saved at {1}.\n").format( SmfNode.pname, filename)) safe_se(result[1]) sys.exit(1) # Manifest validated successfully. Write it to the appropriate # location. outfile = params.get(NM_OUTPUT_FILE) if outfile is not None: try: output = open(outfile, "w") except IOError as e: parser.error( gettext('Unable to open "{0}". {1}').format( outfile, os.strerror(e.errno))) else: output = sys.stdout try: channel.seek(0) output.writelines(channel.readlines()) except IOError as e: parser.error( gettext( "I/O error while copying the manifest. {0}").format( os.strerror(e.errno))) if outfile is not None: output.close() # Remove the temporary file. try: channel.close() os.remove(filename) except OSError: # Don't bother to complain if we can't remove a temp file return def main(): output = sys.stdout params = {} bundle = start_bundle() process_command_line(bundle, params) try: bundle.propagate(params) except CLError as err: bundle.parser.error(err.args[0]) if is_manifest(): # Initially write the manifest to a temp file, so that we can run # svccfg validate on it. os.tempnam() gives us a runtime warning # saying that we shouldn't use tempname because of symlink # vulnerabilities. Unfortunately, the recommended alternative, # tmpfile, won't work, because we need a real file name to pass to # svccfg. Thus, we catch the warning. with warnings.catch_warnings(): warnings.simplefilter("ignore", RuntimeWarning) filename = os.tempnam("/tmp", "svcbundle") else: filename = params.get(NM_OUTPUT_FILE) if filename is None: output = sys.stdout else: try: output = open(filename, "w+") except IOError as e: bundle.parser.error( gettext('Unable to open "{0}". {1}').format( filename, os.strerror(e.errno))) try: bundle.document.smf_node.writexml(output, "", " ", "\n") except IOError as e: if e.errno != errno.EPIPE: bundle.parser.error( gettext("Problem in writing output. {0}").format( os.strerror(e.errno))) sys.exit(1) if is_manifest(): validate_and_write(params, bundle.parser, output, filename) else: if filename is not None: output.close() if SmfNode.installing: manifest_import(params, bundle.parser) sys.exit(0) try: main() except KeyboardInterrupt: sys.exit(1)