1 /*
2 * Copyright (C) 2017-2018 Red Hat, Inc.
3 *
4 * Licensed under the GNU Lesser General Public License Version 2.1
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 #include <algorithm>
22 #include <cstdio>
23 #include <cstring>
24 #include <ctime>
25 #include <dirent.h>
26 #include <fstream>
27 #include <functional>
28 #include <map>
29 #include <memory>
30 #include <stdexcept>
31 #include <string>
32 #include <map>
33 #include <vector>
34 #include <sstream>
35
36 #include "../utils/bgettext/bgettext-lib.h"
37 #include "../utils/filesystem.hpp"
38 #include "../utils/utils.hpp"
39
40 #include "RPMItem.hpp"
41 #include "Swdb.hpp"
42 #include "Transaction.hpp"
43 #include "TransactionItem.hpp"
44 #include "Transformer.hpp"
45
46 namespace libdnf {
47
48 static const char *sql_create_tables =
49 #include "sql/create_tables.sql"
50 ;
51
52 static const char * const sql_migrate_tables_1_2 =
53 #include "sql/migrate_tables_1_2.sql"
54 ;
55
56 void
createDatabase(SQLite3Ptr conn)57 Transformer::createDatabase(SQLite3Ptr conn)
58 {
59 conn->exec(sql_create_tables);
60 Transformer::migrateSchema(conn);
61 }
62
63 void
migrateSchema(SQLite3Ptr conn)64 Transformer::migrateSchema(SQLite3Ptr conn)
65 {
66 // read schema version
67 SQLite3::Query query(*conn, "select value from config where key = 'version';");
68 if (query.step() == SQLite3::Statement::StepResult::ROW){
69 auto schemaVersion = query.get<std::string>("value");
70
71 if (schemaVersion == "1.1") {
72 conn->exec(sql_migrate_tables_1_2);
73 }
74 }
75 else {
76 throw Exception(_("Database Corrupted: no row 'version' in table 'config'"));
77 }
78 }
79
80 /**
81 * Map of supported actions (originally states): string -> enum
82 */
83 static const std::map<std::string, TransactionItemAction > actions = {
84 {"Install", TransactionItemAction::INSTALL},
85 {"True-Install", TransactionItemAction::INSTALL},
86 {"Dep-Install", TransactionItemAction::INSTALL},
87 {"Downgrade", TransactionItemAction::DOWNGRADE},
88 {"Downgraded", TransactionItemAction::DOWNGRADED},
89 {"Obsoleting", TransactionItemAction::OBSOLETE},
90 {"Obsoleted", TransactionItemAction::OBSOLETED},
91 {"Update", TransactionItemAction::UPGRADE},
92 {"Updated", TransactionItemAction::UPGRADED},
93 {"Erase", TransactionItemAction::REMOVE},
94 {"Reinstall", TransactionItemAction::REINSTALL},
95 {"Reinstalled", TransactionItemAction::REINSTALL}};
96
97 /**
98 * Map of supported reasons: string -> enum
99 */
100 static const std::map< std::string, TransactionItemReason > reasons = {
101 {"dep", TransactionItemReason::DEPENDENCY},
102 {"user", TransactionItemReason::USER},
103 {"clean", TransactionItemReason::CLEAN},
104 {"weak", TransactionItemReason::WEAK_DEPENDENCY},
105 {"group", TransactionItemReason::GROUP}};
106
107 /**
108 * Convert string reason into appropriate enumerated variant
109 */
110 TransactionItemReason
getReason(const std::string & reason)111 Transformer::getReason(const std::string &reason)
112 {
113 auto it = reasons.find(reason);
114 if (it == reasons.end()) {
115 return TransactionItemReason::UNKNOWN;
116 }
117 return it->second;
118 }
119
120 /**
121 * Default constructor of the Transformer object
122 * \param outputFile path to output SQLite3 database
123 * \param inputDir directory to load data from (e.g. `/var/lib/dnf/`)
124 */
Transformer(const std::string & inputDir,const std::string & outputFile)125 Transformer::Transformer(const std::string &inputDir, const std::string &outputFile)
126 : inputDir(inputDir)
127 , outputFile(outputFile)
128 {
129 }
130
131 /**
132 * Perform the database transformation routine.
133 * The database is transformed in-memory.
134 * Final scheme is dumped into outputFile
135 */
136 void
transform()137 Transformer::transform()
138 {
139 auto swdb = std::make_shared< SQLite3 >(":memory:");
140
141 if (pathExists(outputFile.c_str())) {
142 throw std::runtime_error("DB file already exists:" + outputFile);
143 }
144
145 // create directory path if necessary
146 makeDirPath(outputFile);
147
148 // create a new database file
149 createDatabase(swdb);
150
151 // migrate history db if it exists
152 try {
153 // make a copy of source database to make creating indexes temporary
154 auto history = std::make_shared< SQLite3 >(":memory:");
155 history->restore(historyPath().c_str());
156
157 // create additional indexes in the source database to increase conversion speed
158 history->exec("CREATE INDEX IF NOT EXISTS i_trans_cmdline_tid ON trans_cmdline(tid);");
159 history->exec("CREATE INDEX IF NOT EXISTS i_trans_data_pkgs_tid ON trans_data_pkgs(tid);");
160 history->exec("CREATE INDEX IF NOT EXISTS i_trans_script_stdout_tid ON trans_script_stdout(tid);");
161 history->exec("CREATE INDEX IF NOT EXISTS i_trans_with_pkgs_tid_pkgtupid ON trans_with_pkgs(tid, pkgtupid);");
162
163 // transform objects
164 transformTrans(swdb, history);
165
166 // transform groups
167 transformGroups(swdb);
168 }
169 catch (Exception &) {
170 // TODO: use a different (more specific) exception
171 }
172
173 // dump database to a file
174 swdb->backup(outputFile);
175 }
176
177 /**
178 * Transform transactions from the history database
179 * \param swdb pointer to swdb SQLite3 object
180 * \param swdb pointer to history database SQLite3 object
181 */
182 void
transformTrans(SQLite3Ptr swdb,SQLite3Ptr history)183 Transformer::transformTrans(SQLite3Ptr swdb, SQLite3Ptr history)
184 {
185 std::vector< std::shared_ptr< TransformerTransaction > > result;
186
187 // we need to left join with trans_cmdline
188 // there is no cmdline for certain transactions (e.g. 1)
189 const char *trans_sql = R"**(
190 SELECT
191 tb.tid as id,
192 tb.timestamp as dt_begin,
193 tb.rpmdb_version rpmdb_version_begin,
194 tb.loginuid as user_id,
195 te.timestamp as dt_end,
196 te.rpmdb_version as rpmdb_version_end,
197 te.return_code as state,
198 tc.cmdline as cmdline
199 FROM
200 trans_beg tb
201 JOIN trans_end te using(tid)
202 LEFT JOIN trans_cmdline tc using(tid)
203 ORDER BY
204 tb.tid
205 )**";
206
207 const char *releasever_sql = R"**(
208 SELECT DISTINCT
209 trans_data_pkgs.tid as tid,
210 yumdb_val as releasever
211 FROM
212 trans_data_pkgs
213 JOIN
214 pkg_yumdb USING (pkgtupid)
215 WHERE
216 yumdb_key='releasever'
217 )**";
218
219 // get release version for all the transactions
220 std::map< int64_t, std::string > releasever;
221 SQLite3::Query releasever_query(*history.get(), releasever_sql);
222 while (releasever_query.step() == SQLite3::Statement::StepResult::ROW) {
223 std::string releaseVerStr = releasever_query.get< std::string >("releasever");
224 releasever[releasever_query.get< int64_t >("tid")] = releaseVerStr;
225 }
226
227 // iterate over history transactions
228 SQLite3::Query query(*history.get(), trans_sql);
229 while (query.step() == SQLite3::Statement::StepResult::ROW) {
230 auto trans = std::make_shared< TransformerTransaction >(swdb);
231 trans->setId(query.get< int >("id"));
232 trans->setDtBegin(query.get< int64_t >("dt_begin"));
233 trans->setDtEnd(query.get< int64_t >("dt_end"));
234 trans->setRpmdbVersionBegin(query.get< std::string >("rpmdb_version_begin"));
235 trans->setRpmdbVersionEnd(query.get< std::string >("rpmdb_version_end"));
236
237 // set release version if available
238 auto it = releasever.find(trans->getId());
239 if (it != releasever.end()) {
240 trans->setReleasever(it->second);
241 }
242
243 trans->setUserId(query.get< int >("user_id"));
244 trans->setCmdline(query.get< std::string >("cmdline"));
245
246 TransactionState state = query.get< int >("state") == 0 ? TransactionState::DONE : TransactionState::ERROR;
247
248 transformRPMItems(swdb, history, trans);
249 transformTransWith(swdb, history, trans);
250
251 trans->begin();
252
253 transformOutput(history, trans);
254
255 trans->finish(state);
256 }
257 }
258
259 static void
fillRPMItem(std::shared_ptr<RPMItem> rpm,SQLite3::Query & query)260 fillRPMItem(std::shared_ptr< RPMItem > rpm, SQLite3::Query &query)
261 {
262 rpm->setName(query.get< std::string >("name"));
263 rpm->setEpoch(query.get< int64_t >("epoch"));
264 rpm->setVersion(query.get< std::string >("version"));
265 rpm->setRelease(query.get< std::string >("release"));
266 rpm->setArch(query.get< std::string >("arch"));
267 rpm->save();
268 }
269
270 /**
271 * Transform binding between a Transaction and packages, which performed the transaction.
272 * \param swdb pointer to swdb SQLite3 object
273 * \param swdb pointer to history database SQLite3 object
274 */
275 void
transformTransWith(SQLite3Ptr swdb,SQLite3Ptr history,std::shared_ptr<TransformerTransaction> trans)276 Transformer::transformTransWith(SQLite3Ptr swdb,
277 SQLite3Ptr history,
278 std::shared_ptr< TransformerTransaction > trans)
279 {
280 const char *sql = R"**(
281 SELECT
282 name,
283 epoch,
284 version,
285 release,
286 arch
287 FROM
288 trans_with_pkgs
289 JOIN pkgtups using (pkgtupid)
290 WHERE
291 tid=?
292 )**";
293
294 // transform stdout
295 SQLite3::Query query(*history.get(), sql);
296 query.bindv(trans->getId());
297 while (query.step() == SQLite3::Statement::StepResult::ROW) {
298 // create RPM item object
299 auto rpm = std::make_shared< RPMItem >(swdb);
300 fillRPMItem(rpm, query);
301 trans->addSoftwarePerformedWith(rpm);
302 }
303 }
304
305 /**
306 * Transform transaction console outputs.
307 * \param swdb pointer to history database SQLite3 object
308 */
309 void
transformOutput(SQLite3Ptr history,std::shared_ptr<TransformerTransaction> trans)310 Transformer::transformOutput(SQLite3Ptr history, std::shared_ptr< TransformerTransaction > trans)
311 {
312 const char *sql = R"**(
313 SELECT
314 line
315 FROM
316 trans_script_stdout
317 WHERE
318 tid = ?
319 ORDER BY
320 lid
321 )**";
322
323 // transform stdout
324 SQLite3::Query query(*history.get(), sql);
325 query.bindv(trans->getId());
326 while (query.step() == SQLite3::Statement::StepResult::ROW) {
327 trans->addConsoleOutputLine(1, query.get< std::string >("line"));
328 }
329
330 sql = R"**(
331 SELECT
332 msg
333 FROM
334 trans_error
335 WHERE
336 tid = ?
337 ORDER BY
338 mid
339 )**";
340
341 // transform stderr
342 SQLite3::Query errorQuery(*history.get(), sql);
343 errorQuery.bindv(trans->getId());
344 while (errorQuery.step() == SQLite3::Statement::StepResult::ROW) {
345 trans->addConsoleOutputLine(2, errorQuery.get< std::string >("msg"));
346 }
347 }
348
349 static void
getYumdbData(int64_t itemId,SQLite3Ptr history,TransactionItemReason & reason,std::string & repoid)350 getYumdbData(int64_t itemId, SQLite3Ptr history, TransactionItemReason &reason, std::string &repoid)
351 {
352 const char *sql = R"**(
353 SELECT
354 yumdb_key as key,
355 yumdb_val as value
356 FROM
357 pkg_yumdb
358 WHERE
359 pkgtupid=?
360 and key IN ('reason', 'from_repo')
361 )**";
362
363 // load reason and repoid data from yumdb
364 SQLite3::Query query(*history.get(), sql);
365 query.bindv(itemId);
366 while (query.step() == SQLite3::Statement::StepResult::ROW) {
367 std::string key = query.get< std::string >("key");
368 if (key == "reason") {
369 reason = Transformer::getReason(query.get< std::string >("value"));
370 } else if (key == "from_repo") {
371 repoid = query.get< std::string >("value");
372 }
373 }
374 }
375
376 /**
377 * Transform RPM Items from a particular transaction.
378 * \param swdb pointer to swdb SQLite3 object
379 * \param swdb pointer to history database SQLite3 objects
380 * \param trans Transaction whose items should be transformed
381 */
382 void
transformRPMItems(SQLite3Ptr swdb,SQLite3Ptr history,std::shared_ptr<TransformerTransaction> trans)383 Transformer::transformRPMItems(SQLite3Ptr swdb,
384 SQLite3Ptr history,
385 std::shared_ptr< TransformerTransaction > trans)
386 {
387 // the order is important here - its Update, Updated
388 const char *pkg_sql = R"**(
389 SELECT
390 t.state,
391 t.done,
392 r.pkgtupid as id,
393 r.name,
394 r.epoch,
395 r.version,
396 r.release,
397 r.arch
398 FROM
399 trans_data_pkgs t
400 JOIN pkgtups r using(pkgtupid)
401 WHERE
402 t.tid=?
403 )**";
404
405 SQLite3::Query query(*history.get(), pkg_sql);
406 query.bindv(trans->getId());
407
408 TransactionItemPtr last = nullptr;
409
410 /*
411 * Item in a single transaction can be both Obsoleted multiple times and Updated.
412 * We need to keep track of all the obsoleted items,
413 * so we can promote them to Updated in case.
414 * Obsoleted records will be kept in item_replaced table,
415 * so it's always obvious, that particular package was both Obsoleted
416 * and Updated. Technically, we could replace action Obsoleted with action Erase.
417 */
418 std::map< int64_t, TransactionItemPtr > obsoletedItems;
419
420 // iterate over transaction packages in the history database
421 while (query.step() == SQLite3::Statement::StepResult::ROW) {
422
423 // create RPM item object
424 auto rpm = std::make_shared< RPMItem >(swdb);
425 fillRPMItem(rpm, query);
426
427 // get item state/action
428 std::string stateString = query.get< std::string >("state");
429 TransactionItemAction action = actions.at(stateString);
430
431 // `Obsoleting` record is duplicated with previous record (with different action)
432 if (action == TransactionItemAction::OBSOLETE) {
433 continue;
434 }
435
436 // find out if an item was previously obsoleted
437 auto pastObsoleted = obsoletedItems.find(rpm->getId());
438
439 TransactionItemPtr transItem = nullptr;
440
441 if (pastObsoleted == obsoletedItems.end()) {
442 // item hasn't been obsoleted yet
443
444 // load reason and from_repo
445 TransactionItemReason reason = TransactionItemReason::UNKNOWN;
446 std::string repoid;
447 getYumdbData(query.get< int64_t >("id"), history, reason, repoid);
448
449 // add TransactionItem object
450 transItem = trans->addItem(rpm, repoid, action, reason);
451 transItem->setState(query.get< std::string >("done") == "TRUE" ? TransactionItemState::DONE : TransactionItemState::ERROR);
452 } else {
453 // item has been obsoleted - we just need to update the action
454 transItem = pastObsoleted->second;
455 transItem->setAction(action);
456 }
457
458 // resolve replaced by
459 switch (action) {
460 case TransactionItemAction::OBSOLETED:
461 obsoletedItems[rpm->getId()] = transItem;
462 transItem->addReplacedBy(last);
463 break;
464 case TransactionItemAction::DOWNGRADED:
465 case TransactionItemAction::UPGRADED:
466 transItem->addReplacedBy(last);
467 break;
468 default:
469 break;
470 }
471
472 // keep the last item in case of obsoletes
473 last = transItem;
474 }
475 }
476
477 /**
478 * Construct CompsGroupItem object from JSON
479 * \param group group json object
480 */
481 CompsGroupItemPtr
processGroup(SQLite3Ptr swdb,const char * groupId,struct json_object * group)482 Transformer::processGroup(SQLite3Ptr swdb, const char *groupId, struct json_object *group)
483 {
484 struct json_object *value;
485
486 // create group
487 auto compsGroup = std::make_shared< CompsGroupItem >(swdb);
488
489 compsGroup->setGroupId(groupId);
490
491 if (json_object_object_get_ex(group, "name", &value)) {
492 compsGroup->setName(json_object_get_string(value));
493 }
494
495 if (json_object_object_get_ex(group, "ui_name", &value)) {
496 compsGroup->setTranslatedName(json_object_get_string(value));
497 }
498
499 // TODO parse pkg_types to CompsPackageType
500 if (json_object_object_get_ex(group, "full_list", &value)) {
501 int len = json_object_array_length(value);
502 for (int i = 0; i < len; ++i) {
503 const char *key = json_object_get_string(json_object_array_get_idx(value, i));
504 compsGroup->addPackage(key, true, CompsPackageType::MANDATORY);
505 }
506 }
507
508 // TODO parse pkg_types to CompsPackageType
509 if (json_object_object_get_ex(group, "pkg_exclude", &value)) {
510 int len = json_object_array_length(value);
511 for (int i = 0; i < len; ++i) {
512 const char *key = json_object_get_string(json_object_array_get_idx(value, i));
513 compsGroup->addPackage(key, false, CompsPackageType::MANDATORY);
514 }
515 }
516
517 compsGroup->save();
518 return compsGroup;
519 }
520
521 /**
522 * Construct CompsEnvironmentItem object from JSON
523 * \param env environment json object
524 */
525 std::shared_ptr< CompsEnvironmentItem >
processEnvironment(SQLite3Ptr swdb,const char * envId,struct json_object * env)526 Transformer::processEnvironment(SQLite3Ptr swdb, const char *envId, struct json_object *env)
527 {
528 struct json_object *value;
529
530 // create environment
531 auto compsEnv = std::make_shared< CompsEnvironmentItem >(swdb);
532 compsEnv->setEnvironmentId(envId);
533
534 if (json_object_object_get_ex (env, "name", &value)) {
535 compsEnv->setName(json_object_get_string(value));
536 }
537
538 if (json_object_object_get_ex (env, "ui_name", &value)) {
539 compsEnv->setTranslatedName(json_object_get_string(value));
540 }
541
542 // TODO parse pkg_types/grp_types to CompsPackageType
543 if (json_object_object_get_ex(env, "full_list", &value)) {
544 int len = json_object_array_length(value);
545 for (int i = 0; i < len; ++i) {
546 const char *key = json_object_get_string(json_object_array_get_idx(value, i));
547 compsEnv->addGroup(key, true, CompsPackageType::MANDATORY);
548 }
549 }
550
551 // TODO parse pkg_types/grp_types to CompsPackageType
552 if (json_object_object_get_ex(env, "pkg_exclude", &value)) {
553 int len = json_object_array_length(value);
554 for (int i = 0; i < len; ++i) {
555 const char *key = json_object_get_string(json_object_array_get_idx(value, i));
556 compsEnv->addGroup(key, false, CompsPackageType::MANDATORY);
557 }
558 }
559
560 compsEnv->save();
561
562 return compsEnv;
563 }
564
565 /**
566 * Create fake transaction for groups in persistor
567 * \param swdb pointer to swdb SQLite3 object
568 * \param root group persistor root node
569 */
570 void
processGroupPersistor(SQLite3Ptr swdb,struct json_object * root)571 Transformer::processGroupPersistor(SQLite3Ptr swdb, struct json_object *root)
572 {
573 // there is no rpmdb change in this transaction,
574 // use rpmdb version from the last converted transaction
575 Swdb swdbObj(swdb, false);
576 auto lastTrans = swdbObj.getLastTransaction();
577
578 auto trans = swdb_private::Transaction(swdb);
579
580 // load sequences
581 struct json_object *groups;
582 struct json_object *envs;
583
584 // add groups
585 if (json_object_object_get_ex(root, "GROUPS", &groups)) {
586 json_object_object_foreach(groups, key, val) {
587 trans.addItem(processGroup (swdb, key, val),
588 {}, // repoid
589 TransactionItemAction::INSTALL,
590 TransactionItemReason::USER);
591 }
592 }
593
594 // add environments
595 if (json_object_object_get_ex(root, "ENVIRONMENTS", &envs)) {
596 json_object_object_foreach(envs, key, val) {
597 trans.addItem(processEnvironment (swdb, key, val),
598 {}, // repoid
599 TransactionItemAction::INSTALL,
600 TransactionItemReason::USER);
601 }
602 }
603
604 trans.begin();
605
606 auto now = time(NULL);
607 trans.setDtBegin(now);
608 trans.setDtEnd(now);
609
610 if (lastTrans) {
611 trans.setRpmdbVersionBegin(lastTrans->getRpmdbVersionEnd());
612 trans.setRpmdbVersionEnd(trans.getRpmdbVersionBegin());
613 } else {
614 // no transaction found -> use 0 packages + hash for an empty string
615 trans.setRpmdbVersionBegin("0:da39a3ee5e6b4b0d3255bfef95601890afd80709");
616 trans.setRpmdbVersionEnd(trans.getRpmdbVersionBegin());
617 }
618
619 for (auto i : trans.getItems()) {
620 i->setState(TransactionItemState::DONE);
621 i->save();
622 }
623
624 trans.finish(TransactionState::DONE);
625 }
626
627 /**
628 * Load group persistor into JSON object and perform transformation
629 * \param swdb pointer to swdb SQLite3 object
630 */
631 void
transformGroups(SQLite3Ptr swdb)632 Transformer::transformGroups(SQLite3Ptr swdb)
633 {
634 std::string groupsFile(inputDir);
635
636 // create the groups.json path
637 if (groupsFile.back() != '/') {
638 groupsFile += '/';
639 }
640 groupsFile += "groups.json";
641
642 std::ifstream groupsStream(groupsFile);
643
644 if (!groupsStream.is_open()) {
645 return;
646 }
647
648 std::stringstream buffer;
649 buffer << groupsStream.rdbuf();
650
651 struct json_object *root = json_tokener_parse(buffer.str().c_str());
652
653 processGroupPersistor(swdb, root);
654 }
655
656 /**
657 * Try to find the history database in the inputDir
658 * \return path to the latest history database in the inputDir
659 */
660 std::string
historyPath()661 Transformer::historyPath()
662 {
663 std::string historyDir(inputDir);
664
665 // construct the history directory path
666 if (historyDir.back() != '/') {
667 historyDir += '/';
668 }
669 historyDir += "history";
670
671 // vector for possible history DB files
672 std::vector< std::string > possibleFiles;
673
674 // open history directory
675 struct dirent *dp;
676 std::unique_ptr<DIR, std::function<void(DIR *)>> dirp(opendir(historyDir.c_str()), [](DIR* ptr){
677 closedir(ptr);
678 });
679
680 if (!dirp) {
681 throw Exception(_("Transformer: can't open history persist dir"));
682 }
683
684 // iterate over history directory and look for 'history-*.sqlite' files
685 while ((dp = readdir(dirp.get())) != nullptr) {
686 std::string fileName(dp->d_name);
687 if (libdnf::string::startsWith(fileName, "history-") &&
688 libdnf::string::endsWith(fileName, ".sqlite")) {
689 possibleFiles.push_back(fileName);
690 }
691 }
692
693 if (possibleFiles.empty()) {
694 throw Exception(_("Couldn't find a history database"));
695 }
696
697 // find the latest DB file
698 std::sort(possibleFiles.begin(), possibleFiles.end());
699
700 // return the path
701 return historyDir + "/" + possibleFiles.back();
702 }
703
704 } // namespace libdnf
705