Odyssey
ody_migr_executor.py
1 #!/usr/bin/env python
2 """Main module to execute Mammoth to Odyssey Data Migration.
3 
4 usage: ody_migr_executor.py [-h] [--secret SECRET] [--reset_and_migrate]
5  [--verbose] [--disable_logging]
6  {www4,www3,www5,www6} {clean,migrate,summary} cu
7  {settings,memdata,memhist,admin} username password
8 
9 Argument Parser for Mammoth to Odyssey Data Migration.
10 
11 positional arguments:
12  {www4,www3,www5,www6}
13  server code for Mammoth endpoint
14  {clean,migrate,summary}
15  migration action
16  cu target Credit Union <CUCODE> to migrate (upper case
17  required)
18  {settings,memdata,memhist,admin,loanapp}
19  data category to migrate
20  username mammoth monitor username
21  password mammoth monitor password (base64 encoded)
22 
23 optional arguments:
24  -h, --help show this help message and exit
25  --secret SECRET, -s SECRET
26  pass secret key as optional argument instead
27  (alternative to environment variable)
28  --reset_and_migrate, -reset
29  clean db tables and commit before migration)
30  --verbose, -v output verbosity
31  --disable_logging, -dl
32  disable logging, logs only critical messages
33 
34 
35 Requirements:
36  Python>=3.4
37  Main packages:
38  psycopg2>=2.7.4
39  requests
40 
41 Note (06/14/2018):
42 All the data migration scripts(app/tools/bin/ody_migr_*.py) have a shelf life.
43 Their usage is limited to one-time data migration of CU and its members from
44 Mammoth to Odyssey. We do not intend to maintain these scripts as-is after
45 migrations are complete.
46 """
47 
48 
49 import sys
50 import os
51 import grp
52 import pwd
53 import argparse
54 import logging
55 import base64
56 
57 from logging import FileHandler, StreamHandler, Formatter
58 from ody_migr_settings import migrate_settings
59 from ody_migr_members import migrate_members
60 from ody_migr_admin import migrate_admin
61 from ody_migr_loans import migrate_loanapps
62 from ody_migr_utils import file_exc_decorator
63 from ody_migr_mmth_endpoint import MammothMigration
64 
65 import ody_migr_db_handler as pg_handler
66 from psycopg2.extras import RealDictCursor
67 from ody_migr_transaction import pg_crsr_hndlr_decrtr
68 
69 from ody_migr_config import (ENV_MIGR_SECRET_KEY,
70  SERVER_CHOICES,
71  ACTION_CHOICES,
72  DATA_CHOICES,
73  DATA_OPT_SETTINGS,
74  DATA_OPT_MEMDATA,
75  DATA_OPT_SWITCH_ACCOUNTS,
76  DATA_OPT_MEMHIST,
77  DATA_OPT_ADMIN,
78  DATA_OPT_LOANAPP,
79  DATA_OPT_GETCULIST,
80  SUPPORTED_DEV_CU,
81  LOG_FILE_NAME,
82  SYS_TYPE_BATCH,
83  SYS_TYPE_CLOSED,
84  SYS_TYPE_LIVE,
85  SYS_TYPE_WEBONLY)
86 
87 LOGGER = logging.getLogger(__name__)
88 
89 # dictionary of main entrypoints for major data categories
90 MIGRATION_ENTRYPOINTS = {
91  DATA_OPT_SETTINGS: migrate_settings,
92  DATA_OPT_MEMDATA: migrate_members,
93  DATA_OPT_SWITCH_ACCOUNTS: migrate_members,
94  DATA_OPT_MEMHIST: migrate_members,
95  DATA_OPT_ADMIN: migrate_admin,
96  DATA_OPT_LOANAPP: migrate_loanapps
97 }
98 
99 
100 @file_exc_decorator
101 def get_ownership_detail(_directory):
102  """get file/directory user/group ownership detail"""
103  stat_info = os.stat(_directory)
104  user = pwd.getpwuid(stat_info.st_uid)[0]
105  group = grp.getgrgid(stat_info.st_gid)[0]
106  return user, group
107 
108 
109 @file_exc_decorator
110 def owned_file_handler(_file_name, mode='w', owner=None):
111  """Return a logging file handler with specified user/group ownership"""
112  if owner:
113  import pwd
114  import grp
115  # convert user and group names to uid and gid
116  uid = pwd.getpwnam(owner[0]).pw_uid
117  gid = grp.getgrnam(owner[1]).gr_gid
118  owner = (uid, gid)
119  if not os.path.exists(_file_name):
120  open(_file_name, 'a').close()
121  os.chown(_file_name, *owner)
122  return FileHandler(_file_name, mode)
123 
124 
125 @file_exc_decorator
126 def setup_logging(_verbose,
127  _disable_logging,
128  formatter,
129  _cu,
130  _server,
131  _log_file):
132  """Setup logging configuration and handlers."""
133  log_file_dir = os.path.dirname(_log_file)
134 
135  # if log directory/file does not exist
136  if not os.path.exists(log_file_dir):
137  err_msg = ("Log directory `{}` does not exist. "
138  "MAKE SURE OF THE FOLLOWING: "
139  "(i) `/home/{}/*` directories are in place (perhaps, `{}` "
140  "is not created in Odyssey?). "
141  "(ii) CUCODE `{}` is a valid Mammoth target for migration "
142  "in `{}`."
143  "(iii) CUCODE `{}` is an intended Odyssey source "
144  "for migration.").format(
145  log_file_dir,
146  _cu.lower(),
147  _cu.upper(),
148  _cu.upper(),
149  _server,
150  _cu.upper())
151  raise Exception(err_msg)
152 
153  log_dir_ownership = get_ownership_detail(log_file_dir)
154 
155  fh = owned_file_handler(_log_file, mode="w", owner=log_dir_ownership)
156  fh.setFormatter(formatter)
157  LOGGER.addHandler(fh)
158  handlers = [fh]
159 
160  if(_disable_logging):
161  logging.basicConfig(level=logging.CRITICAL, handlers=handlers)
162 
163  else:
164  if _verbose:
165  logging.basicConfig(level=logging.DEBUG, handlers=handlers)
166  else:
167  logging.basicConfig(level=logging.INFO, handlers=handlers)
168 
169 
171  """Argument parser for the main executor"""
172  parser = argparse.ArgumentParser(
173  description="Argument Parser for Mammoth to Odyssey Data Migration.")
174 
175  parser.add_argument(
176  "server",
177  type=str,
178  choices=SERVER_CHOICES,
179  help="server code for Mammoth endpoint"
180  )
181 
182  parser.add_argument(
183  "action",
184  type=str,
185  choices=ACTION_CHOICES,
186  help="migration action"
187  )
188 
189  parser.add_argument(
190  "cu",
191  type=str,
192  help="target Credit Union <CUCODE> to migrate (upper case required)"
193  )
194 
195  parser.add_argument(
196  "data_category",
197  type=str,
198  choices=DATA_CHOICES,
199  help="data category to migrate"
200  )
201 
202  parser.add_argument(
203  "username",
204  type=str,
205  help="mammoth monitor username"
206  )
207 
208  parser.add_argument(
209  "password",
210  type=str,
211  help="mammoth monitor password (base64 encoded)"
212  )
213 
214  parser.add_argument(
215  "--secret",
216  "-s",
217  type=str,
218  help=("pass secret key as optional argument instead "
219  "(alternative to environment variable)")
220  )
221 
222  parser.add_argument(
223  "--reset_and_migrate",
224  "-reset",
225  help="clean db tables and commit before migration)",
226  action="store_true"
227  )
228 
229  parser.add_argument(
230  "--verbose",
231  "-v",
232  help="output verbosity",
233  action="store_true"
234  )
235 
236  parser.add_argument(
237  "--disable_logging",
238  "-dl",
239  help="disable logging, logs only critical messages",
240  action="store_true"
241  )
242 
243  return parser
244 
245 
246 @pg_crsr_hndlr_decrtr
248  """Return a list of valid CUs from cuinfo"""
249  sql = ("select user_name, www_server, case "
250  " when (coalesce(system_options, 0) & {}) <> 0 then 'Live'"
251  " when (coalesce(system_options, 0) & {}) <> 0 then 'Batch'"
252  " when (coalesce(system_options, 0) & {}) <> 0 then 'Web Only'"
253  " else 'Other' end as cutype from cuinfo"
254  " where (coalesce(system_options, 0) & {}) = 0"
255  " order by user_name".format(SYS_TYPE_LIVE,
256  SYS_TYPE_BATCH,
257  SYS_TYPE_WEBONLY,
258  SYS_TYPE_CLOSED))
259  valid_cu_list = []
260  with pg_handler.PGSession() as conn:
261  with conn.cursor(cursor_factory=RealDictCursor) as cur:
262  cur.execute(sql)
263  valid_cu_list = cur.fetchall()
264  return valid_cu_list
265 
266 
267 def run():
268  """Main entrypoint for all the migration operations."""
269  argv = sys.argv[1:]
270  args = get_parser().parse_args(argv)
271 
272  # verify that necessary env variables are exported
273  if os.getenv(ENV_MIGR_SECRET_KEY) is None:
274  if args.secret is None:
275  error_msg = ("SECRET KEY is required (either supply as --secret"
276  " argument or set `{}` env variable.)".format(
277  ENV_MIGR_SECRET_KEY))
278  LOGGER.error(error_msg)
279  raise SystemExit(error_msg)
280  os.environ[ENV_MIGR_SECRET_KEY] = args.secret.strip()
281 
282  if os.getenv(ENV_MIGR_SECRET_KEY) is None:
283  error_msg = "Environment variable: `{}` is not set!".format(
284  ENV_MIGR_SECRET_KEY)
285  LOGGER.error(error_msg)
286  raise SystemExit(error_msg)
287 
288  requested_cu = args.cu.strip()
289  # log formatter
290  log_formatter = Formatter('%(asctime)s '
291  '%(levelname)-4s [{}-{}-{}-{}] '
292  '(%(name)s|%(funcName)s:%(lineno)d) :: '
293  '%(message)s'
294  .format(args.server.strip().upper(),
295  args.data_category.strip().upper(),
296  args.action.strip().upper(),
297  requested_cu))
298 
299  # fill up LOG_FILE_NAME structure
300  log_file = LOG_FILE_NAME.format(
301  requested_cu.lower(),
302  args.action.strip().lower(),
303  args.data_category.strip().lower())
304 
305  # setup logging
306  setup_logging(args.verbose,
307  args.disable_logging,
308  log_formatter,
309  requested_cu,
310  args.server.strip(),
311  log_file)
312 
313  # log file exists (does not necessarily mean that requested_cu has been
314  # set up appropriatedly in Odyssey for migration)
315  LOGGER.info("MIGRATION STARTED: [Data Category: {}, "
316  "Operation: {}, CU: {}]".format(
317  args.data_category.strip().upper(),
318  args.action.strip().upper(),
319  requested_cu))
320 
321  # validate CU and servers for action=migrate only
322  if args.action.strip() == "migrate":
323  # Check if the requested_cu and servers are valid
324  if requested_cu in SUPPORTED_DEV_CU and args.server.strip() == "www4":
325  LOGGER.warning("Relaxing validation for DEV CUs. "
326  "Allowed DEV CUs are: {}".format(SUPPORTED_DEV_CU))
327  else:
328  allow_cu_migration = False
329  valid_cu_info = {}
330 
331  all_cu_list_monitor = get_valid_cu_list()
332 
333  for cu_info in all_cu_list_monitor:
334  # initially, we are only allowing "Live" CU's for migration
335  if cu_info['cutype'].strip() in ["Live"] and cu_info[
336  'user_name'].strip().upper() == requested_cu and cu_info[
337  'www_server'].strip() == args.server.strip():
338  allow_cu_migration = True
339  valid_cu_info = cu_info
340  break
341  else:
342  continue
343 
344  if(allow_cu_migration):
345  LOGGER.info("CUCODE `{}` [{}] is valid for "
346  "server `{}` in production."
347  .format(
348  valid_cu_info['user_name'].strip(),
349  valid_cu_info['cutype'].strip(),
350  valid_cu_info['www_server'].strip())
351  )
352  else:
353  error_msg = ("CUCODE `{}` is not valid for "
354  "server `{}` in production."
355  .format(requested_cu, args.server.strip()))
356  LOGGER.error(error_msg)
357  raise SystemExit(error_msg)
358 
359  # when running the scripts individually in the web-container,
360  # passwords are being displayed as raw text; this is just a
361  # simple attempt to mask the exact strings
362  args.password = base64.b64decode(args.password.strip()).decode()
363 
364  # prepare arguments for migration
365  args_migration = [
366  args.cu.strip(),
367  args.server.strip(),
368  args.action.strip(),
369  args.username.strip(),
370  args.password,
371  args.verbose
372  ]
373 
374  # adjust arguments for memdata and memhist
375  if args.data_category in [DATA_OPT_MEMHIST, DATA_OPT_MEMDATA,
376  DATA_OPT_SWITCH_ACCOUNTS]:
377  args_migration.append(args.data_category)
378  args_migration.append(args.reset_and_migrate)
379 
380  # adjust arguments for admin
381  elif args.data_category in [DATA_OPT_ADMIN, DATA_OPT_LOANAPP]:
382  args_migration.append(args.reset_and_migrate)
383 
384  # start migration
385  migration_exec_resp = MIGRATION_ENTRYPOINTS[args.data_category](*args_migration)
386 
387  LOGGER.info("MIGRATION DONE: [Data Category: {}, "
388  "Operation: {}, CU: {}]".format(
389  args.data_category.strip().upper(),
390  args.action.strip().upper(),
391  args.cu.strip()))
392  return migration_exec_resp
393 
394 
395 if __name__ == "__main__":
396  sys.exit(run())
def setup_logging(_verbose, _disable_logging, formatter, _cu, _server, _log_file)
def owned_file_handler(_file_name, mode='w', owner=None)
def get_ownership_detail(_directory)