1
0
mirror of https://github.com/ARM-software/workload-automation.git synced 2025-01-18 20:11:20 +00:00

Add WA Create Database Command

Add a command to create a PostgreSQL database with supplied parameters
which draws its structure from the supplied schema (Version 1.1). This
database is of a format intended to be used with the forthcoming WA
Postgres output processor.
This commit is contained in:
Waleed El-Geresy 2018-07-20 15:14:51 +01:00 committed by Marc Bonnici
parent ca03f21f46
commit bb255de9ad
4 changed files with 388 additions and 2 deletions

5
setup.py Normal file → Executable file
View File

@ -31,7 +31,7 @@ wa_dir = os.path.join(os.path.dirname(__file__), 'wa')
sys.path.insert(0, os.path.join(wa_dir, 'framework')) sys.path.insert(0, os.path.join(wa_dir, 'framework'))
from version import get_wa_version, get_wa_version_with_commit from version import get_wa_version, get_wa_version_with_commit
# happends if falling back to distutils # happens if falling back to distutils
warnings.filterwarnings('ignore', "Unknown distribution option: 'install_requires'") warnings.filterwarnings('ignore', "Unknown distribution option: 'install_requires'")
warnings.filterwarnings('ignore', "Unknown distribution option: 'extras_require'") warnings.filterwarnings('ignore', "Unknown distribution option: 'extras_require'")
@ -41,7 +41,7 @@ except OSError:
pass pass
packages = [] packages = []
data_files = {} data_files = {'': [os.path.join(wa_dir, 'commands', 'postgres_schema.sql')]}
source_dir = os.path.dirname(__file__) source_dir = os.path.dirname(__file__)
for root, dirs, files in os.walk(wa_dir): for root, dirs, files in os.walk(wa_dir):
rel_dir = os.path.relpath(root, source_dir) rel_dir = os.path.relpath(root, source_dir)
@ -67,6 +67,7 @@ params = dict(
version=get_wa_version_with_commit(), version=get_wa_version_with_commit(),
packages=packages, packages=packages,
package_data=data_files, package_data=data_files,
include_package_data=True,
scripts=scripts, scripts=scripts,
url='https://github.com/ARM-software/workload-automation', url='https://github.com/ARM-software/workload-automation',
license='Apache v2', license='Apache v2',

View File

@ -13,16 +13,26 @@
# limitations under the License. # limitations under the License.
# #
import os import os
import sys import sys
import stat import stat
import shutil import shutil
import string import string
import re
import uuid
import getpass import getpass
from collections import OrderedDict from collections import OrderedDict
from distutils.dir_util import copy_tree # pylint: disable=no-name-in-module, import-error from distutils.dir_util import copy_tree # pylint: disable=no-name-in-module, import-error
from devlib.utils.types import identifier from devlib.utils.types import identifier
try:
import psycopg2
from psycopg2 import connect, OperationalError, extras
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
except ImportError as e:
psycopg2 = None
import_error_msg = e.args[0] if e.args else str(e)
from wa import ComplexCommand, SubCommand, pluginloader, settings from wa import ComplexCommand, SubCommand, pluginloader, settings
from wa.framework.target.descriptor import list_target_descriptions from wa.framework.target.descriptor import list_target_descriptions
@ -36,6 +46,142 @@ from wa.utils.serializer import yaml
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), 'templates') TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), 'templates')
class CreateDatabaseSubcommand(SubCommand):
name = 'database'
description = """
Create a Postgresql database which is compatible with the WA Postgres
output processor.
"""
schemafilepath = 'postgres_schema.sql'
def __init__(self, *args, **kwargs):
super(CreateDatabaseSubcommand, self).__init__(*args, **kwargs)
self.sql_commands = None
self.schemaversion = None
self.schema_major = None
self.schema_minor = None
def initialize(self, context):
self.parser.add_argument(
'-a', '--postgres-host', default='localhost',
help='The host on which to create the database.')
self.parser.add_argument(
'-k', '--postgres-port', default='5432',
help='The port on which the PostgreSQL server is running.')
self.parser.add_argument(
'-u', '--username', default='postgres',
help='The username with which to connect to the server.')
self.parser.add_argument(
'-p', '--password',
help='The password for the user account.')
self.parser.add_argument(
'-d', '--dbname', default='wa',
help='The name of the database to create.')
self.parser.add_argument(
'-f', '--force', action='store_true',
help='Force overwrite the existing database if one exists.')
self.parser.add_argument(
'-F', '--force-update-config', action='store_true',
help='Force update the config file if an entry exists.')
self.parser.add_argument(
'-r', '--config-file', default=settings.user_config_file,
help='Path to the config file to be updated.')
self.parser.add_argument(
'-x', '--schema-version', action='store_true',
help='Display the current schema version.')
def execute(self, state, args): # pylint: disable=too-many-branches
if not psycopg2:
raise CommandError(
'The module psycopg2 is required for the wa ' +
'create database command.')
self.get_schema(self.schemafilepath)
# Display the version if needed and exit
if args.schema_version:
self.logger.info(
'The current schema version is {}'.format(self.schemaversion))
return
if args.dbname == 'postgres':
raise ValueError('Databasename to create cannot be postgres.')
# Open user configuration
with open(args.config_file, 'r') as config_file:
config = yaml.load(config_file)
if 'postgres' in config and not args.force_update_config:
raise CommandError(
"The entry 'postgres' already exists in the config file. " +
"Please specify the -F flag to force an update.")
possible_connection_errors = [
(
re.compile('FATAL: role ".*" does not exist'),
'Username does not exist or password is incorrect'
),
(
re.compile('FATAL: password authentication failed for user'),
'Password was incorrect'
),
(
re.compile('fe_sendauth: no password supplied'),
'Passwordless connection is not enabled. '
'Please enable trust in pg_hba for this host '
'or use a password'
),
(
re.compile('FATAL: no pg_hba.conf entry for'),
'Host is not allowed to connect to the specified database '
'using this user according to pg_hba.conf. Please change the '
'rules in pg_hba or your connection method'
),
(
re.compile('FATAL: pg_hba.conf rejects connection'),
'Connection was rejected by pg_hba.conf'
),
]
def predicate(error, handle):
if handle[0].match(str(error)):
raise CommandError(handle[1] + ': \n' + str(error))
# Attempt to create database
try:
self.create_database(args)
except OperationalError as e:
for handle in possible_connection_errors:
predicate(e, handle)
raise e
# Update the configuration file
_update_configuration_file(args, config)
def create_database(self, args):
_check_database_existence(args)
_create_database_postgres(args)
_apply_database_schema(args, self.sql_commands, self.schema_major, self.schema_minor)
self.logger.debug(
"Successfully created the database {}".format(args.dbname))
def get_schema(self, schemafilepath):
postgres_output_processor_dir = os.path.dirname(__file__)
sqlfile = open(os.path.join(
postgres_output_processor_dir, schemafilepath))
self.sql_commands = sqlfile.read()
sqlfile.close()
# Extract schema version
if self.sql_commands.startswith('--!VERSION'):
splitcommands = self.sql_commands.split('!ENDVERSION!\n')
self.schemaversion = splitcommands[0].strip('--!VERSION!')
(self.schema_major, self.schema_minor) = self.schemaversion.split('.')
self.sql_commands = splitcommands[1]
class CreateAgendaSubcommand(SubCommand): class CreateAgendaSubcommand(SubCommand):
name = 'agenda' name = 'agenda'
@ -181,6 +327,7 @@ class CreateCommand(ComplexCommand):
object-specific arguments. object-specific arguments.
''' '''
subcmd_classes = [ subcmd_classes = [
CreateDatabaseSubcommand,
CreateWorkloadSubcommand, CreateWorkloadSubcommand,
CreateAgendaSubcommand, CreateAgendaSubcommand,
CreatePackageSubcommand, CreatePackageSubcommand,
@ -280,3 +427,62 @@ def get_class_name(name, postfix=''):
def touch(path): def touch(path):
with open(path, 'w') as _: # NOQA with open(path, 'w') as _: # NOQA
pass pass
def _check_database_existence(args):
try:
connect(dbname=args.dbname, user=args.username,
password=args.password, host=args.postgres_host, port=args.postgres_port)
except OperationalError as e:
# Expect an operational error (database's non-existence)
if not re.compile('FATAL: database ".*" does not exist').match(str(e)):
raise e
else:
if not args.force:
raise CommandError(
"Database {} already exists. ".format(args.dbname) +
"Please specify the -f flag to create it from afresh."
)
def _create_database_postgres(args): # pylint: disable=no-self-use
conn = connect(dbname='postgres', user=args.username,
password=args.password, host=args.postgres_host, port=args.postgres_port)
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cursor = conn.cursor()
cursor.execute('DROP DATABASE IF EXISTS ' + args.dbname)
cursor.execute('CREATE DATABASE ' + args.dbname)
conn.commit()
cursor.close()
conn.close()
def _apply_database_schema(args, sql_commands, schema_major, schema_minor):
conn = connect(dbname=args.dbname, user=args.username,
password=args.password, host=args.postgres_host, port=args.postgres_port)
cursor = conn.cursor()
cursor.execute(sql_commands)
extras.register_uuid()
cursor.execute("INSERT INTO DatabaseMeta VALUES (%s, %s, %s)",
(
uuid.uuid4(),
schema_major,
schema_minor
)
)
conn.commit()
cursor.close()
conn.close()
def _update_configuration_file(args, config):
''' Update the user configuration file with the newly created database's
configuration.
'''
config['postgres'] = OrderedDict(
[('host', args.postgres_host), ('port', args.postgres_port),
('dbname', args.dbname), ('username', args.username), ('password', args.password)])
with open(args.config_file, 'w+') as config_file:
yaml.dump(config, config_file)

View File

@ -0,0 +1,170 @@
--!VERSION!1.1!ENDVERSION!
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "lo";
-- In future, it may be useful to implement rules on which Parameter oid fields can be none depeendent on the value in the type column;
DROP TABLE IF EXISTS DatabaseMeta;
DROP TABLE IF EXISTS Parameters;
DROP TABLE IF EXISTS Classifiers;
DROP TABLE IF EXISTS LargeObjects;
DROP TABLE IF EXISTS Artifacts;
DROP TABLE IF EXISTS Metrics;
DROP TABLE IF EXISTS Augmentations;
DROP TABLE IF EXISTS Jobs_Augs;
DROP TABLE IF EXISTS ResourceGetters;
DROP TABLE IF EXISTS Events;
DROP TABLE IF EXISTS Targets;
DROP TABLE IF EXISTS Jobs;
DROP TABLE IF EXISTS Runs;
DROP TYPE IF EXISTS status_enum;
DROP TYPE IF EXISTS param_enum;
CREATE TYPE status_enum AS ENUM ('UNKNOWN(0)','NEW(1)','PENDING(2)','STARTED(3)','CONNECTED(4)', 'INITIALIZED(5)', 'RUNNING(6)', 'OK(7)', 'PARTIAL(8)', 'FAILED(9)', 'ABORTED(10)', 'SKIPPED(11)');
CREATE TYPE param_enum AS ENUM ('workload', 'resource_getter', 'augmentation', 'device', 'runtime', 'boot');
-- In future, it might be useful to create an ENUM type for the artifact kind, or simply a generic enum type;
CREATE TABLE DatabaseMeta (
oid uuid NOT NULL,
schema_major int,
schema_minor int,
PRIMARY KEY (oid)
);
CREATE TABLE Runs (
oid uuid NOT NULL,
event_summary text,
basepath text,
status status_enum,
timestamp timestamp,
run_name text,
project text,
retry_on_status status_enum[],
max_retries int,
bail_on_init_failure boolean,
allow_phone_home boolean,
run_uuid uuid,
start_time timestamp,
end_time timestamp,
metadata jsonb,
PRIMARY KEY (oid)
);
CREATE TABLE Jobs (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
status status_enum,
retries int,
label text,
job_id text,
iterations int,
workload_name text,
metadata jsonb,
PRIMARY KEY (oid)
);
CREATE TABLE Targets (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
target text,
cpus text[],
os text,
os_version jsonb,
hostid int,
hostname text,
abi text,
is_rooted boolean,
kernel_version text,
kernel_release text,
kernel_sha1 text,
kernel_config text[],
sched_features text[],
PRIMARY KEY (oid)
);
CREATE TABLE Events (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
job_oid uuid references Jobs(oid),
timestamp timestamp,
message text,
PRIMARY KEY (oid)
);
CREATE TABLE ResourceGetters (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
name text,
PRIMARY KEY (oid)
);
CREATE TABLE Augmentations (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
name text,
PRIMARY KEY (oid)
);
CREATE TABLE Jobs_Augs (
oid uuid NOT NULL,
job_oid uuid NOT NULL references Jobs(oid),
augmentation_oid uuid NOT NULL references Augmentations(oid),
PRIMARY KEY (oid)
);
CREATE TABLE Metrics (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
job_oid uuid references Jobs(oid),
name text,
value double precision,
units text,
lower_is_better boolean,
PRIMARY KEY (oid)
);
CREATE TABLE LargeObjects (
oid uuid NOT NULL,
lo_oid lo NOT NULL,
PRIMARY KEY (oid)
);
-- Trigger that allows you to manage large objects from the LO table directly;
CREATE TRIGGER t_raster BEFORE UPDATE OR DELETE ON LargeObjects
FOR EACH ROW EXECUTE PROCEDURE lo_manage(lo_oid);
CREATE TABLE Artifacts (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
job_oid uuid references Jobs(oid),
name text,
large_object_uuid uuid NOT NULL references LargeObjects(oid),
description text,
kind text,
PRIMARY KEY (oid)
);
CREATE TABLE Classifiers (
oid uuid NOT NULL,
artifact_oid uuid references Artifacts(oid),
metric_oid uuid references Metrics(oid),
key text,
value text,
PRIMARY KEY (oid)
);
CREATE TABLE Parameters (
oid uuid NOT NULL,
run_oid uuid NOT NULL references Runs(oid),
job_oid uuid references Jobs(oid),
augmentation_oid uuid references Augmentations(oid),
resource_getter_oid uuid references ResourceGetters(oid),
name text,
value text,
value_type text,
type param_enum,
PRIMARY KEY (oid)
);

View File

@ -0,0 +1,9 @@
# 1
## 1.0
- First version
## 1.1
- LargeObjects table added as a substitute for the previous plan to
use the filesystem and a path reference to store artifacts. This
was done following an extended discussion and tests that verified
that the savings in processing power were not enough to warrant
the creation of a dedicated server or file handler.