| Current File : //bin/svcbundle |
#!/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}</{1}>{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}<!--\n".format(indent))
self.wrapper.initial_indent = indent + addindent
self.wrapper.subsequent_indent = self.wrapper.initial_indent
writer.write(self.wrapper.fill(node.data))
writer.write("{0}{1}-->{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('<?xml version="1.0" ?>' + 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 <n> as $<n>. 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 = <undefined>
$7 = <undefined>
$8 = #Related
$9 = Related
where <undefined> 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}(?<!-)$"
comp_domain_re = re.compile(domain_re, re.IGNORECASE)
ident_re = r"[A-Za-z][_A-Za-z0-9-]*"
comp_ident_re = re.compile(ident_re)
name_re = r"([A-Za-z][_A-Za-z0-9.-]*,)?[A-Za-z][_A-Za-z0-9-]*"
comp_name_re = re.compile(name_re)
service_re = name_re + r"(/" + name_re + r")*"
comp_service_re = re.compile(service_re)
uri_re = r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"
comp_uri_re = re.compile(uri_re)
def __init__(self, value, params):
"""Count this instance and add value to the params dictionary.
Count the instance and make sure that we have not exceeded the
maximum for the instance. If the maximum is not exceeded call
update_params() to add the value to the params dictionary.
"""
count = self.__class__._count
count += 1
max_inst = self._max
if (max_inst >= 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)